Repository: hikari-py/hikari
Pull request #2196 (2025-03-06 15:54:36)

Components V2!
### Summary
<!-- Small summary of the merge request -->
Adds support for components V2!

### Checklist
<!-- Make sure to tick all the following boxes by putting an `x` in between (like this `[x]`) -->
- [x] I have run `nox` and all the pipelines have passed.
- [x] I have made unittests according to the code I have added/modified/deleted.

### Related issues
<!--
To mention an issue use `#issue-id` and to mention a merge request use `!merge-request-id`
To close/fix an issue use `Close #issue-id` or `Fix #issue-id` (depending on the merge request)
-->


davfsa: `spoiler` fields should also be renamed to `is_spoiler`

Patch:
diff --git a/changes/2196.feature.md b/changes/2196.feature.md
new file mode 100644
index 00000000..9eae85aa
--- /dev/null
+++ b/changes/2196.feature.md
@@ -0,0 +1,1 @@
+Add components V2 (UIKit) support
diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py
index 16d45dbf..3c426b43 100644
--- a/hikari/api/special_endpoints.py
+++ b/hikari/api/special_endpoints.py
@@ -28,7 +28,9 @@
     "ChannelSelectMenuBuilder",
     "CommandBuilder",
     "ComponentBuilder",
+    "ContainerComponentBuilder",
     "ContextMenuCommandBuilder",
+    "FileComponentBuilder",
     "InteractionAutocompleteBuilder",
     "InteractionDeferredBuilder",
     "InteractionMessageBuilder",
@@ -36,15 +38,21 @@
     "InteractionResponseBuilder",
     "InteractiveButtonBuilder",
     "LinkButtonBuilder",
+    "MediaGalleryComponentBuilder",
+    "MediaGalleryItemBuilder",
     "MessageActionRowBuilder",
     "ModalActionRowBuilder",
     "PollAnswerBuilder",
     "PollBuilder",
+    "SectionComponentBuilder",
     "SelectMenuBuilder",
     "SelectOptionBuilder",
+    "SeparatorComponentBuilder",
     "SlashCommandBuilder",
+    "TextDisplayComponentBuilder",
     "TextInputBuilder",
     "TextSelectMenuBuilder",
+    "ThumbnailComponentBuilder",
     "TypingIndicator",
 )
 
@@ -63,6 +71,7 @@
 
     from hikari import applications
     from hikari import channels
+    from hikari import colors
     from hikari import commands
     from hikari import embeds as embeds_
     from hikari import emojis
@@ -995,14 +1004,26 @@ class ComponentBuilder(abc.ABC):
     def type(self) -> int | components_.ComponentType:
         """Type of component this builder represents."""
 
+    @property
     @abc.abstractmethod
-    def build(self) -> typing.MutableMapping[str, typing.Any]:
-        """Build a JSON object from this builder.
+    def id(self) -> undefined.UndefinedOr[int]:
+        """ID of the component.
+
+        It will be used to order components in the layout.
+
+        If not provided, it will be auto populated through increment.
+        """
+
+    @abc.abstractmethod
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
+        """Build a JSON object from this builder and collects all attachments added as components.
 
         Returns
         -------
-        typing.MutableMapping[str, typing.Any]
-            The built json object representation of this builder.
+        tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]
+            The built json object representation of this builder, and the attachments added.
         """
 
 
@@ -1639,11 +1660,11 @@ def type(self) -> typing.Literal[components_.ComponentType.ACTION_ROW]:
 
     @property
     @abc.abstractmethod
-    def components(self) -> typing.Sequence[ComponentBuilder]:
+    def components(self) -> typing.Sequence[MessageActionRowBuilderComponentsT]:
         """Sequence of the component builders registered within this action row."""
 
     @abc.abstractmethod
-    def add_component(self, component: ComponentBuilder, /) -> Self:
+    def add_component(self, component: MessageActionRowBuilderComponentsT, /) -> Self:
         """Add a component to this action row builder.
 
         !!! warning
@@ -1673,6 +1694,7 @@ def add_interactive_button(
         emoji: snowflakes.Snowflakeish | emojis.Emoji | str | undefined.UndefinedType = undefined.UNDEFINED,
         label: undefined.UndefinedOr[str] = undefined.UNDEFINED,
         is_disabled: bool = False,
+        id: undefined.UndefinedOr[int] = undefined.UNDEFINED,
     ) -> Self:
         """Add an interactive button component to this action row builder.
 
@@ -1692,6 +1714,10 @@ def add_interactive_button(
             The button's display label.
         is_disabled
             Whether the button should be marked as disabled.
+        id
+            The ID to give to the button.
+
+            If not provided, auto populated through increment.
 
         Returns
         -------
@@ -1708,6 +1734,7 @@ def add_link_button(
         emoji: snowflakes.Snowflakeish | emojis.Emoji | str | undefined.UndefinedType = undefined.UNDEFINED,
         label: undefined.UndefinedOr[str] = undefined.UNDEFINED,
         is_disabled: bool = False,
+        id: undefined.UndefinedOr[int] = undefined.UNDEFINED,
     ) -> Self:
         """Add a link button component to this action row builder.
 
@@ -1724,6 +1751,10 @@ def add_link_button(
             The button's display label.
         is_disabled
             Whether the button should be marked as disabled.
+        id
+            The ID to give to the button.
+
+            If not provided, auto populated through increment.
 
         Returns
         -------
@@ -1742,6 +1773,7 @@ def add_select_menu(
         min_values: int = 0,
         max_values: int = 1,
         is_disabled: bool = False,
+        id: undefined.UndefinedOr[int] = undefined.UNDEFINED,
     ) -> Self:
         """Add a select menu component to this action row builder.
 
@@ -1764,6 +1796,10 @@ def add_select_menu(
             The maximum amount of entries which can be selected.
         is_disabled
             Whether this select menu should be marked as disabled.
+        id
+            The ID to give to the menu.
+
+            If not provided, auto populated through increment.
 
         Returns
         -------
@@ -1787,6 +1823,7 @@ def add_channel_menu(
         min_values: int = 0,
         max_values: int = 1,
         is_disabled: bool = False,
+        id: undefined.UndefinedOr[int] = undefined.UNDEFINED,
     ) -> Self:
         """Add a channel select menu component to this action row builder.
 
@@ -1808,6 +1845,10 @@ def add_channel_menu(
             The maximum amount of entries which can be selected.
         is_disabled
             Whether this select menu should be marked as disabled.
+        id
+            The ID to give to the menu.
+
+            If not provided, auto populated through increment.
 
         Returns
         -------
@@ -1830,6 +1871,7 @@ def add_text_menu(
         min_values: int = 0,
         max_values: int = 1,
         is_disabled: bool = False,
+        id: undefined.UndefinedOr[int] = undefined.UNDEFINED,
     ) -> TextSelectMenuBuilder[Self]:
         """Add a select menu component to this action row builder.
 
@@ -1846,6 +1888,10 @@ def add_text_menu(
             The maximum amount of entries which can be selected.
         is_disabled
             Whether this select menu should be marked as disabled.
+        id
+            The ID to give to the menu.
+
+            If not provided, auto populated through increment.
 
         Returns
         -------
@@ -1877,11 +1923,11 @@ def type(self) -> typing.Literal[components_.ComponentType.ACTION_ROW]:
 
     @property
     @abc.abstractmethod
-    def components(self) -> typing.Sequence[ComponentBuilder]:
+    def components(self) -> typing.Sequence[ModalActionRowBuilderComponentsT]:
         """Sequence of the component builders registered within this action row."""
 
     @abc.abstractmethod
-    def add_component(self, component: ComponentBuilder, /) -> Self:
+    def add_component(self, component: ModalActionRowBuilderComponentsT, /) -> Self:
         """Add a component to this action row builder.
 
         !!! warning
@@ -1947,6 +1993,416 @@ def add_text_input(
         """
 
 
+class SectionComponentBuilder(ComponentBuilder, abc.ABC):
+    """Builder class for section components."""
+
+    __slots__: typing.Sequence[str] = ()
+
+    @property
+    @abc.abstractmethod
+    @typing_extensions.override
+    def type(self) -> typing.Literal[components_.ComponentType.SECTION]:
+        """Type of component this builder represents."""
+
+    @property
+    @abc.abstractmethod
+    def components(self) -> typing.Sequence[SectionBuilderComponentsT]:
+        """The components attached to the section."""
+
+    @property
+    @abc.abstractmethod
+    def accessory(self) -> SectionBuilderAccessoriesT:
+        """The accessory attached to the section."""
+
+    @abc.abstractmethod
+    def add_component(self, component: SectionBuilderComponentsT) -> Self:
+        """Add a component to this section builder.
+
+        !!! warning
+            It is generally better to use
+            [`hikari.api.special_endpoints.MessageSectionBuilder.add_text_display`][]
+            to add your component to the builder. Those methods utilize this one.
+
+        Parameters
+        ----------
+        component
+            The component builder to add to the section.
+
+        Returns
+        -------
+        SectionComponentBuilder
+            The builder object to enable chained calls.
+        """
+
+    @abc.abstractmethod
+    def add_text_display(self, content: str, *, id: undefined.UndefinedOr[int] = undefined.UNDEFINED) -> Self:
+        """Add a text display component to this section builder.
+
+        Parameters
+        ----------
+        content
+            The content for the text display.
+        id
+            The ID to give to the text display.
+
+            If not provided, auto populated through increment.
+
+        Returns
+        -------
+        SectionComponentBuilder
+            The builder object to enable chained calls.
+        """
+
+
+class TextDisplayComponentBuilder(ComponentBuilder, abc.ABC):
+    """Builder class for text display components."""
+
+    __slots__: typing.Sequence[str] = ()
+
+    @property
+    @abc.abstractmethod
+    @typing_extensions.override
+    def type(self) -> typing.Literal[components_.ComponentType.TEXT_DISPLAY]:
+        """Type of component this builder represents."""
+
+    @property
+    @abc.abstractmethod
+    def content(self) -> str:
+        """The content for the text display."""
+
+
+class ThumbnailComponentBuilder(ComponentBuilder, abc.ABC):
+    """Builder class for thumbnail components."""
+
+    __slots__: typing.Sequence[str] = ()
+
+    @property
+    @abc.abstractmethod
+    @typing_extensions.override
+    def type(self) -> typing.Literal[components_.ComponentType.THUMBNAIL]:
+        """Type of component this builder represents."""
+
+    @property
+    @abc.abstractmethod
+    def media(self) -> files.Resourceish:
+        """The media of this thumbnail."""
+
+    @property
+    @abc.abstractmethod
+    def description(self) -> undefined.UndefinedOr[str]:
+        """The description for the thumbnails media."""
+
+    @property
+    @abc.abstractmethod
+    def is_spoiler(self) -> bool:
+        """Whether the media is marked as a spoiler."""
+
+
+class MediaGalleryComponentBuilder(ComponentBuilder, abc.ABC):
+    """Builder class for media gallery components."""
+
+    __slots__: typing.Sequence[str] = ()
+
+    @property
+    @abc.abstractmethod
+    @typing_extensions.override
+    def type(self) -> typing.Literal[components_.ComponentType.MEDIA_GALLERY]:
+        """Type of component this builder represents."""
+
+    @property
+    @abc.abstractmethod
+    def items(self) -> typing.Sequence[MediaGalleryItemBuilder]:
+        """The items in the media gallery."""
+
+    @abc.abstractmethod
+    def add_item(self, item: MediaGalleryItemBuilder) -> Self:
+        """Add a component to this media gallery builder.
+
+        !!! warning
+            It is generally better to use
+            [`hikari.api.special_endpoints.MessageMediaGalleryBuilder.add_media_gallery_item`][]
+            to add your component to the builder. Those methods utilize this one.
+
+        Parameters
+        ----------
+        item
+            The media gallery item builder to add to the section.
+
+        Returns
+        -------
+        MediaGalleryComponentBuilder
+            The builder object to enable chained calls.
+        """
+
+    @abc.abstractmethod
+    def add_media_gallery_item(
+        self,
+        media: files.Resourceish,
+        *,
+        description: undefined.UndefinedOr[str] = undefined.UNDEFINED,
+        spoiler: bool = False,
+    ) -> Self:
+        """Add a media gallery item component to this media gallery builder.
+
+        Parameters
+        ----------
+        media
+            The media for the gallery item.
+        description
+            The description for the media gallery item.
+        spoiler
+            Whether the media has a spoiler.
+
+        Returns
+        -------
+        MediaGalleryComponentBuilder
+            The builder object to enable chained calls.
+        """
+
+
+class MediaGalleryItemBuilder(abc.ABC):
+    """Builder class for a media gallery item."""
+
+    __slots__: typing.Sequence[str] = ()
+
+    @property
+    @abc.abstractmethod
+    def media(self) -> files.Resourceish:
+        """The media for the gallery item."""
+
+    @property
+    @abc.abstractmethod
+    def description(self) -> undefined.UndefinedOr[str]:
+        """The description for the media gallery item."""
+
+    @property
+    @abc.abstractmethod
+    def is_spoiler(self) -> bool:
+        """Whether the media is marked as a spoiler."""
+
+    @abc.abstractmethod
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
+        """Build a JSON object from this builder and collects all attachments added as components.
+
+        Returns
+        -------
+        tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]
+            The built json object representation of this builder, and the attachments added.
+        """
+
+
+class SeparatorComponentBuilder(ComponentBuilder, abc.ABC):
+    """Builder class for separator components."""
+
+    __slots__: typing.Sequence[str] = ()
+
+    @property
+    @abc.abstractmethod
+    @typing_extensions.override
+    def type(self) -> typing.Literal[components_.ComponentType.SEPARATOR]:
+        """Type of component this builder represents."""
+
+    @property
+    @abc.abstractmethod
+    def spacing(self) -> undefined.UndefinedOr[components_.SpacingType]:
+        """The spacing for the separator."""
+
+    @property
+    @abc.abstractmethod
+    def divider(self) -> undefined.UndefinedOr[bool]:
+        """Whether the separator has a divider."""
+
+
+class FileComponentBuilder(ComponentBuilder, abc.ABC):
+    """Builder class for file components."""
+
+    __slots__: typing.Sequence[str] = ()
+
+    @property
+    @abc.abstractmethod
+    @typing_extensions.override
+    def type(self) -> typing.Literal[components_.ComponentType.FILE]:
+        """Type of component this builder represents."""
+
+    @property
+    @abc.abstractmethod
+    def file(self) -> files.Resourceish:
+        """The file to attach."""
+
+    @property
+    @abc.abstractmethod
+    def is_spoiler(self) -> bool:
+        """Whether the file has a spoiler."""
+
+
+class ContainerComponentBuilder(ComponentBuilder, abc.ABC):
+    """Builder class for container components."""
+
+    __slots__: typing.Sequence[str] = ()
+
+    @property
+    @abc.abstractmethod
+    @typing_extensions.override
+    def type(self) -> typing.Literal[components_.ComponentType.CONTAINER]:
+        """Type of component this builder represents."""
+
+    @property
+    @abc.abstractmethod
+    def accent_color(self) -> undefined.UndefinedOr[colors.Color]:
+        """The accent color for the container.
+
+        If undefined, the accent colour is hidden, if None, then no colour is set.
+        """
+
+    @property
+    @abc.abstractmethod
+    def is_spoiler(self) -> bool:
+        """Whether the container has a spoiler."""
+
+    @property
+    @abc.abstractmethod
+    def components(self) -> typing.Sequence[ContainerBuilderComponentsT]:
+        """The components attached to the container."""
+
+    @abc.abstractmethod
+    def add_component(self, component: ContainerBuilderComponentsT) -> Self:
+        """Add a component to this container builder.
+
+        !!! warning
+            It is generally better to use
+            [`hikari.api.special_endpoints.MessageContainerBuilder.add_action_row`][]
+            and [`hikari.api.special_endpoints.MessageContainerBuilder.add_text_display`][]
+            and [`hikari.api.special_endpoints.MessageContainerBuilder.add_media_gallery`][]
+            and [`hikari.api.special_endpoints.MessageContainerBuilder.add_separator`][]
+            and [`hikari.api.special_endpoints.MessageContainerBuilder.add_file`][]
+            to add your component to the builder. Those methods utilize this one.
+
+        Parameters
+        ----------
+        component
+            The component builder to add to the container.
+
+        Returns
+        -------
+        ContainerComponentBuilder
+            The builder object to enable chained calls.
+        """
+
+    @abc.abstractmethod
+    def add_action_row(
+        self,
+        components: typing.Sequence[MessageActionRowBuilderComponentsT],
+        *,
+        id: undefined.UndefinedOr[int] = undefined.UNDEFINED,
+    ) -> Self:
+        """Add a action row component to this container builder.
+
+        Parameters
+        ----------
+        components
+            The components to add to the action row.
+        id
+            The ID to give to the action row.
+
+            If not provided, auto populated through increment.
+
+        Returns
+        -------
+        ContainerComponentBuilder
+            The builder object to enable chained calls.
+        """
+
+    @abc.abstractmethod
+    def add_text_display(self, content: str, *, id: undefined.UndefinedOr[int] = undefined.UNDEFINED) -> Self:
+        """Add a text display component to this container builder.
+
+        Parameters
+        ----------
+        content
+            The content of the text display.
+        id
+            The ID to give to the text display.
+
+            If not provided, auto populated through increment.
+
+        Returns
+        -------
+        ContainerComponentBuilder
+            The builder object to enable chained calls.
+        """
+
+    @abc.abstractmethod
+    def add_media_gallery(
+        self, items: typing.Sequence[MediaGalleryItemBuilder], *, id: undefined.UndefinedOr[int] = undefined.UNDEFINED
+    ) -> Self:
+        """Add a media gallery component to this container builder.
+
+        Parameters
+        ----------
+        items
+            The gallery media items to add to the media gallery.
+        id
+            The ID to give to the media gallery.
+
+            If not provided, auto populated through increment.
+
+        Returns
+        -------
+        ContainerComponentBuilder
+            The builder object to enable chained calls.
+        """
+
+    @abc.abstractmethod
+    def add_separator(
+        self,
+        *,
+        spacing: components_.SpacingType = components_.SpacingType.SMALL,
+        divider: bool = False,
+        id: undefined.UndefinedOr[int] = undefined.UNDEFINED,
+    ) -> Self:
+        """Add a separator component to this container builder.
+
+        Parameters
+        ----------
+        spacing
+            The spacing for the separator.
+        divider
+            Whether the separator has a divider.
+        id
+            The ID to give to the separator.
+
+            If not provided, auto populated through increment.
+
+        Returns
+        -------
+        ContainerComponentBuilder
+        """
+
+    @abc.abstractmethod
+    def add_file(
+        self, file: files.Resourceish, *, spoiler: bool = False, id: undefined.UndefinedOr[int] = undefined.UNDEFINED
+    ) -> Self:
+        """Add a spoiler component to this container builder.
+
+        Parameters
+        ----------
+        file
+            The file.
+        spoiler
+            Whether the file has a spoiler.
+        id
+            The ID to give to the file.
+            If not provided, auto populated through increment.
+
+        Returns
+        -------
+        ContainerComponentBuilder
+            The builder object to enable chained calls.
+        """
+
+
 class PollBuilder(abc.ABC):
     """Builder class for polls."""
 
@@ -2034,3 +2490,21 @@ def build(self) -> typing.MutableMapping[str, typing.Any]:
         typing.MutableMapping[str, typing.Any]
             The built json object representation of this builder.
         """
+
+
+if typing.TYPE_CHECKING:
+    MessageActionRowBuilderComponentsT = typing.Union[ButtonBuilder, SelectMenuBuilder]
+
+    ModalActionRowBuilderComponentsT = TextInputBuilder
+
+    ContainerBuilderComponentsT = typing.Union[
+        MessageActionRowBuilder,
+        TextDisplayComponentBuilder,
+        SectionComponentBuilder,
+        MediaGalleryComponentBuilder,
+        SeparatorComponentBuilder,
+        FileComponentBuilder,
+    ]
+
+    SectionBuilderAccessoriesT = typing.Union[ButtonBuilder, ThumbnailComponentBuilder]
+    SectionBuilderComponentsT = typing.Union[TextDisplayComponentBuilder]
diff --git a/hikari/components.py b/hikari/components.py
index 2e25cfc4..fdaa415e 100644
--- a/hikari/components.py
+++ b/hikari/components.py
@@ -28,29 +28,50 @@
     "ButtonStyle",
     "ChannelSelectMenuComponent",
     "ComponentType",
+    "ContainerComponent",
+    "ContainerTypesT",
+    "FileComponent",
     "InteractiveButtonTypes",
     "InteractiveButtonTypesT",
+    "MediaGalleryComponent",
+    "MediaGalleryItem",
+    "MediaLoadingType",
+    "MediaResource",
     "MessageActionRowComponent",
     "MessageComponentTypesT",
     "ModalActionRowComponent",
     "ModalComponentTypesT",
     "PartialComponent",
+    "SectionComponent",
     "SelectMenuComponent",
     "SelectMenuOption",
+    "SeparatorComponent",
+    "SpacingType",
+    "TextDisplayComponent",
     "TextInputComponent",
     "TextInputStyle",
     "TextSelectMenuComponent",
+    "ThumbnailComponent",
+    "TopLevelComponentTypesT",
 )
 
 import typing
 
 import attrs
 
+from hikari import channels
+from hikari import emojis
+from hikari import files
 from hikari.internal import enums
+from hikari.internal import typing_extensions
 
 if typing.TYPE_CHECKING:
+    import concurrent.futures
+
     from hikari import channels
+    from hikari import colors
     from hikari import emojis
+    from hikari import undefined
 
 
 @typing.final
@@ -60,10 +81,6 @@ class ComponentType(int, enums.Enum):
     ACTION_ROW = 1
     """A non-interactive container component for other types of components.
 
-    !!! note
-        As this is a container component it can never be contained within another
-        component and therefore will always be top-level.
-
     !!! note
         As of writing this can only contain one component type.
     """
@@ -127,6 +144,42 @@ class ComponentType(int, enums.Enum):
         as [`hikari.components.ComponentType.ACTION_ROW`][].
     """
 
+    SECTION = 9
+    """A section component.
+
+    !!! note
+        As this is a container component it can never be contained within another
+        component and therefore will always be top-level.
+    """
+
+    TEXT_DISPLAY = 10
+    """A text display component."""
+
+    THUMBNAIL = 11
+    """A thumbnail component.
+
+    !!! note
+        This cannot be top-level and must be within a container component such
+        as [`hikari.components.ComponentType.SECTION`][].
+    """
+
+    MEDIA_GALLERY = 12
+    """A media gallery component."""
+
+    FILE = 13
+    """A file component."""
+
+    SEPARATOR = 14
+    """A separator component."""
+
+    CONTAINER = 17
+    """A container component.
+
+    !!! note
+        As this is a container component it can never be contained within another
+        component and therefore will always be top-level.
+    """
+
 
 @typing.final
 class ButtonStyle(int, enums.Enum):
@@ -168,6 +221,37 @@ class TextInputStyle(int, enums.Enum):
     """Intended for much longer inputs."""
 
 
+@typing.final
+class SpacingType(int, enums.Enum):
+    """Spacing Type.
+
+    The type of spacing for a [SeparatorComponent][]
+    """
+
+    SMALL = 1
+    """A small separator."""
+
+    LARGE = 2
+    """A large separator."""
+
+
+@typing.final
+class MediaLoadingType(int, enums.Enum):
+    """Media loading type."""
+
+    UNKNOWN = 0
+    """Media is in an unknown loading state."""
+
+    LOADING = 1
+    """Media is loading."""
+
+    LOADED_SUCCESS = 2
+    """Media has successfully loaded."""
+
+    LOADED_NOT_FOUND = 3
+    """Media was not found."""
+
+
 @attrs.define(kw_only=True, weakref_slot=False)
 class PartialComponent:
     """Base class for all component entities."""
@@ -175,6 +259,9 @@ class PartialComponent:
     type: ComponentType | int = attrs.field()
     """The type of component this is."""
 
+    id: int = attrs.field()
+    """The ID of the interaction."""
+
 
 AllowedComponentsT = typing.TypeVar("AllowedComponentsT", bound="PartialComponent")
 
@@ -309,6 +396,226 @@ class TextInputComponent(PartialComponent):
     """Value provided for this text input."""
 
 
+@attrs.define(kw_only=True, weakref_slot=False)
+class MediaResource(files.Resource[files.AsyncReader]):
+    """Represents a media resource."""
+
+    resource: files.Resource[files.AsyncReader] = attrs.field(repr=True)
+    """The resource this object wraps around."""
+
+    proxy_resource: files.Resource[files.AsyncReader] | None = attrs.field(default=None, repr=False)
+    """The proxied version of the resource, or [`None`][] if not present.
+
+    !!! note
+        This field cannot be set by bots or webhooks while sending an embed
+        and will be ignored during serialization. Expect this to be
+        populated on any received embed attached to a message event.
+    """
+
+    width: undefined.UndefinedNoneOr[int] = attrs.field(repr=True)
+    """The width of media item."""
+
+    height: undefined.UndefinedNoneOr[int] = attrs.field(repr=True)
+    """The height of the media item."""
+
+    content_type: undefined.UndefinedNoneOr[str] = attrs.field(repr=True)
+    """The content type of the media item."""
+
+    loading_state: undefined.UndefinedNoneOr[MediaLoadingType] = attrs.field(repr=True)
+    """The loading state of the media item."""
+
+    @property
+    @typing_extensions.override
+    def url(self) -> str:
+        """URL of this embed resource."""
+        return self.resource.url
+
+    @property
+    @typing_extensions.override
+    def filename(self) -> str:
+        """File name of this embed resource."""
+        return self.resource.filename
+
+    @property
+    def proxy_url(self) -> str | None:
+        """Proxied URL of this embed resource if applicable, else [`None`][]."""
+        return self.proxy_resource.url if self.proxy_resource else None
+
+    @property
+    def proxy_filename(self) -> str | None:
+        """File name of the proxied version of this embed resource if applicable, else [`None`][]."""
+        return self.proxy_resource.filename if self.proxy_resource else None
+
+    @typing_extensions.override
+    def stream(
+        self, *, executor: concurrent.futures.Executor | None = None, head_only: bool = False
+    ) -> files.AsyncReaderContextManager[files.AsyncReader]:
+        """Produce a stream of data for the resource.
+
+        Parameters
+        ----------
+        executor
+            The executor to run in for blocking operations.
+            If [`None`][], then the default executor is used for the
+            current event loop.
+        head_only
+            If [`True`][], then the implementation may only retrieve
+            HEAD information if supported. This currently only has
+            any effect for web requests.
+        """
+        return self.resource.stream(executor=executor, head_only=head_only)
+
+
+@attrs.define(kw_only=True, weakref_slot=False)
+class SectionComponent(PartialComponent):
+    """Represents a section component."""
+
+    components: typing.Sequence[SectionComponentTypesT] = attrs.field()
+    """The sections components."""
+
+    accessory: SectionAccessoryTypesT = attrs.field()
+    """The sections accessory."""
+
+
+@attrs.define(kw_only=True, weakref_slot=False)
+class ThumbnailComponent(PartialComponent):
+    """Represents a thumbnail component."""
+
+    media: MediaResource = attrs.field()
+    """The media for the thumbnail."""
+
+    description: str | None = attrs.field()
+    """The description of the thumbnail."""
+
+    is_spoiler: bool = attrs.field()
+    """Whether the thumbnail is marked as a spoiler."""
+
+
+@attrs.define(kw_only=True, weakref_slot=False)
+class TextDisplayComponent(PartialComponent):
+    """Represents a text display component."""
+
+    content: str = attrs.field()
+    """The content of the text display."""
+
+
+@attrs.define(kw_only=True, weakref_slot=False)
+class MediaGalleryComponent(PartialComponent):
+    """Represents a media gallery component."""
+
+    items: typing.Sequence[MediaGalleryItem] = attrs.field()
+    """The media gallery's items."""
+
+
+@attrs.define(kw_only=True, weakref_slot=False)
+class MediaGalleryItem:
+    """Represents a media gallery item."""
+
+    media: MediaResource = attrs.field()
+    """The media for the gallery item."""
+
+    description: str | None = attrs.field()
+    """The description of the gallery item."""
+
+    is_spoiler: bool = attrs.field()
+    """Whether the gallery item is marked as a spoiler."""
+
+
+@attrs.define(kw_only=True, weakref_slot=False)
+class SeparatorComponent(PartialComponent):
+    """Represents the separator component."""
+
+    spacing: SpacingType = attrs.field()
+    """The spacing for the separator."""
+
+    divider: bool = attrs.field()
+    """If there is a divider for the separator."""
+
+
+@attrs.define(kw_only=True, weakref_slot=False)
+class FileComponent(PartialComponent):
+    """Represents a file component."""
+
+    file: MediaResource = attrs.field()
+    """The media for the file."""
+
+    is_spoiler: bool = attrs.field()
+    """If the file has a spoiler."""
+
+
+@attrs.define(kw_only=True, weakref_slot=False)
+class ContainerComponent(PartialComponent):
+    """Represents a container component."""
+
+    accent_color: colors.Color | None = attrs.field()
+    """The accent colour for the container."""
+
+    is_spoiler: bool = attrs.field()
+    """Whether the container is marked as a spoiler."""
+
+    components: typing.Sequence[ContainerTypesT] = attrs.field()
+    """The components within the container."""
+
+
+TopLevelComponentTypesT = typing.Union[
+    ActionRowComponent[PartialComponent],
+    TextDisplayComponent,
+    SectionComponent,
+    MediaGalleryComponent,
+    SeparatorComponent,
+    FileComponent,
+    ContainerComponent,
+]
+"""Type hints of the values which are valid for top level components.
+
+The following values are valid for this:
+
+* [`hikari.components.ActionRowComponent`][]
+* [`hikari.components.TextDisplayComponent`][]
+* [`hikari.components.SectionComponent`][]
+* [`hikari.components.MediaGalleryComponent`][]
+* [`hikari.components.SeparatorComponent`][]
+* [`hikari.components.FileComponent`][]
+* [`hikari.components.ContainerComponent`][]
+"""
+
+ContainerTypesT = typing.Union[
+    ActionRowComponent[PartialComponent],
+    TextDisplayComponent,
+    SectionComponent,
+    MediaGalleryComponent,
+    SeparatorComponent,
+    FileComponent,
+]
+"""Type hints of the values which are valid for container components.
+
+The following values are valid for this:
+
+* [`hikari.components.ActionRowComponent`][]
+* [`hikari.components.TextDisplayComponent`][]
+* [`hikari.components.SectionComponent`][]
+* [`hikari.components.MediaGalleryComponent`][]
+* [`hikari.components.SeparatorComponent`][]
+* [`hikari.components.FileComponent`][]
+"""
+
+SectionComponentTypesT = TextDisplayComponent
+"""Type hints of the values which are valid for section components.
+
+The following values are valid for this:
+
+* [`hikari.components.TextDisplayComponent`][]
+"""
+
+SectionAccessoryTypesT = typing.Union[ButtonComponent, ThumbnailComponent]
+"""Type hints of the values which are valid for section accessories.
+
+The following values are valid for this:
+
+* [`hikari.components.ButtonComponent`][]
+* [`hikari.components.ThumbnailComponent`][]
+"""
+
 SelectMenuTypesT = typing.Literal[
     ComponentType.TEXT_SELECT_MENU,
     3,
diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py
index 99476cc5..2a6e162a 100644
--- a/hikari/impl/entity_factory.py
+++ b/hikari/impl/entity_factory.py
@@ -446,18 +446,22 @@ class EntityFactoryImpl(entity_factory.EntityFactory):
     """
 
     __slots__: typing.Sequence[str] = (
+        "_action_row_component_type_mapping",
         "_app",
         "_audit_log_entry_converters",
         "_audit_log_event_mapping",
         "_command_mapping",
+        "_container_component_mapping",
         "_dm_channel_type_mapping",
         "_guild_channel_type_mapping",
         "_interaction_metadata_mapping",
         "_interaction_type_mapping",
-        "_message_component_type_mapping",
         "_modal_component_type_mapping",
         "_scheduled_event_type_mapping",
+        "_section_accessory_mapping",
+        "_section_component_mapping",
         "_thread_channel_type_mapping",
+        "_top_level_components_mapping",
         "_webhook_type_mapping",
     )
 
@@ -524,7 +528,7 @@ def __init__(self, app: traits.RESTAware) -> None:
             commands.CommandType.USER: self.deserialize_context_menu_command,
             commands.CommandType.MESSAGE: self.deserialize_context_menu_command,
         }
-        self._message_component_type_mapping: dict[
+        self._action_row_component_type_mapping: dict[
             int, typing.Callable[[data_binding.JSONObject], component_models.MessageComponentTypesT]
         ] = {
             component_models.ComponentType.BUTTON: self._deserialize_button,
@@ -534,6 +538,38 @@ def __init__(self, app: traits.RESTAware) -> None:
             component_models.ComponentType.MENTIONABLE_SELECT_MENU: self._deserialize_select_menu,
             component_models.ComponentType.CHANNEL_SELECT_MENU: self._deserialize_channel_select_menu,
         }
+        self._top_level_components_mapping: dict[
+            component_models.ComponentType,
+            typing.Callable[[data_binding.JSONObject], component_models.TopLevelComponentTypesT],
+        ] = {
+            component_models.ComponentType.ACTION_ROW: self._deserialize_action_row_component,
+            component_models.ComponentType.SECTION: self._deserialize_section_component,
+            component_models.ComponentType.TEXT_DISPLAY: self._deserialize_text_display_component,
+            component_models.ComponentType.MEDIA_GALLERY: self._deserialize_media_gallery_component,
+            component_models.ComponentType.SEPARATOR: self._deserialize_separator_component,
+            component_models.ComponentType.FILE: self._deserialize_file_component,
+            component_models.ComponentType.CONTAINER: self._deserialize_container_component,
+        }
+        self._container_component_mapping: dict[
+            component_models.ComponentType, typing.Callable[[data_binding.JSONObject], component_models.ContainerTypesT]
+        ] = {
+            component_models.ComponentType.SECTION: self._deserialize_section_component,
+            component_models.ComponentType.TEXT_DISPLAY: self._deserialize_text_display_component,
+            component_models.ComponentType.MEDIA_GALLERY: self._deserialize_media_gallery_component,
+            component_models.ComponentType.SEPARATOR: self._deserialize_separator_component,
+            component_models.ComponentType.FILE: self._deserialize_file_component,
+        }
+        self._section_component_mapping: dict[
+            component_models.ComponentType,
+            typing.Callable[[data_binding.JSONObject], component_models.SectionComponentTypesT],
+        ] = {component_models.ComponentType.TEXT_DISPLAY: self._deserialize_text_display_component}
+        self._section_accessory_mapping: dict[
+            component_models.ComponentType,
+            typing.Callable[[data_binding.JSONObject], component_models.SectionAccessoryTypesT],
+        ] = {
+            component_models.ComponentType.THUMBNAIL: self._deserialize_thumbnail_component,
+            component_models.ComponentType.BUTTON: self._deserialize_button,
+        }
         self._modal_component_type_mapping: dict[
             int, typing.Callable[[data_binding.JSONObject], component_models.ModalComponentTypesT]
         ] = {component_models.ComponentType.TEXT_INPUT: self._deserialize_text_input}
@@ -2852,7 +2888,7 @@ def deserialize_modal_interaction(self, payload: data_binding.JSONObject) -> mod
             token=payload["token"],
             version=payload["version"],
             custom_id=data_payload["custom_id"],
-            components=self._deserialize_components(data_payload["components"], self._modal_component_type_mapping),
+            components=self._deserialize_modal_components(data_payload["components"]),
             message=message,
             entitlements=[self.deserialize_entitlement(entitlement) for entitlement in payload.get("entitlements", ())],
             authorizing_integration_owners=authorizing_integration_owners,
@@ -3029,26 +3065,10 @@ def deserialize_guild_sticker(self, payload: data_binding.JSONObject) -> sticker
     # COMPONENT MODELS #
     ####################
 
-    @typing.overload
-    def _deserialize_components(
-        self,
-        payloads: data_binding.JSONArray,
-        mapping: dict[int, typing.Callable[[data_binding.JSONObject], component_models.MessageComponentTypesT]],
-    ) -> list[component_models.MessageActionRowComponent]: ...
-
-    @typing.overload
-    def _deserialize_components(
-        self,
-        payloads: data_binding.JSONArray,
-        mapping: dict[int, typing.Callable[[data_binding.JSONObject], component_models.ModalComponentTypesT]],
-    ) -> list[component_models.ModalActionRowComponent]: ...
-
-    def _deserialize_components(
-        self,
-        payloads: data_binding.JSONArray,
-        mapping: dict[int, typing.Callable[[data_binding.JSONObject], typing.Any]],
-    ) -> list[component_models.ActionRowComponent[typing.Any]]:
-        top_level_components = []
+    def _deserialize_modal_components(
+        self, payloads: data_binding.JSONArray
+    ) -> typing.Sequence[component_models.ModalActionRowComponent]:
+        top_level_components: list[component_models.ModalActionRowComponent] = []
 
         for payload in payloads:
             top_level_component_type = component_models.ComponentType(payload["type"])
@@ -3057,12 +3077,12 @@ def _deserialize_components(
                 _LOGGER.debug("Unknown top-level message component type %s", top_level_component_type)
                 continue
 
-            components = []
+            components: list[component_models.ModalComponentTypesT] = []
 
             for component_payload in payload["components"]:
                 component_type = component_models.ComponentType(component_payload["type"])
 
-                if (deserializer := mapping.get(component_type)) is None:
+                if (deserializer := self._modal_component_type_mapping.get(component_type)) is None:
                     _LOGGER.debug("Unknown component type %s", component_type)
                     continue
 
@@ -3072,15 +3092,34 @@ def _deserialize_components(
                 # If we somehow get a top-level component full of unknown components, ignore the top-level
                 # component all-together
                 top_level_components.append(
-                    component_models.ActionRowComponent(type=top_level_component_type, components=components)
+                    component_models.ActionRowComponent(
+                        type=top_level_component_type, id=payload["id"], components=components
+                    )
                 )
 
         return top_level_components
 
+    def _deserialize_top_level_components(
+        self, payloads: data_binding.JSONArray
+    ) -> typing.Sequence[component_models.TopLevelComponentTypesT]:
+        top_level_components: list[component_models.TopLevelComponentTypesT] = []
+
+        for payload in payloads:
+            top_level_component_type = component_models.ComponentType(payload["type"])
+
+            if deserializer := self._top_level_components_mapping.get(top_level_component_type):
+                top_level_components.append(deserializer(payload))
+            else:
+                _LOGGER.debug("Unknown component type %s", top_level_component_type)
+                continue
+
+        return top_level_components
+
     def _deserialize_button(self, payload: data_binding.JSONObject) -> component_models.ButtonComponent:
         emoji_payload = payload.get("emoji")
         return component_models.ButtonComponent(
             type=component_models.ComponentType(payload["type"]),
+            id=payload["id"],
             style=component_models.ButtonStyle(payload["style"]),
             label=payload.get("label"),
             emoji=self.deserialize_emoji(emoji_payload) if emoji_payload else None,
@@ -3092,6 +3131,7 @@ def _deserialize_button(self, payload: data_binding.JSONObject) -> component_mod
     def _deserialize_select_menu(self, payload: data_binding.JSONObject) -> component_models.SelectMenuComponent:
         return component_models.SelectMenuComponent(
             type=component_models.ComponentType(payload["type"]),
+            id=payload["id"],
             custom_id=payload["custom_id"],
             placeholder=payload.get("placeholder"),
             min_values=payload.get("min_values", 1),
@@ -3121,6 +3161,7 @@ def _deserialize_text_select_menu(
 
         return component_models.TextSelectMenuComponent(
             type=component_models.ComponentType(payload["type"]),
+            id=payload["id"],
             custom_id=payload["custom_id"],
             options=options,
             placeholder=payload.get("placeholder"),
@@ -3138,6 +3179,7 @@ def _deserialize_channel_select_menu(
 
         return component_models.ChannelSelectMenuComponent(
             type=component_models.ComponentType(payload["type"]),
+            id=payload["id"],
             custom_id=payload["custom_id"],
             channel_types=channel_types,
             placeholder=payload.get("placeholder"),
@@ -3148,7 +3190,158 @@ def _deserialize_channel_select_menu(
 
     def _deserialize_text_input(self, payload: data_binding.JSONObject) -> component_models.TextInputComponent:
         return component_models.TextInputComponent(
-            type=component_models.ComponentType(payload["type"]), custom_id=payload["custom_id"], value=payload["value"]
+            type=component_models.ComponentType(payload["type"]),
+            id=payload["id"],
+            custom_id=payload["custom_id"],
+            value=payload["value"],
+        )
+
+    def _deserialize_media(self, payload: data_binding.JSONObject) -> component_models.MediaResource:
+        height: undefined.UndefinedNoneOr[int] = undefined.UNDEFINED
+        if "height" in payload:
+            height = payload.get("height")
+
+        width: undefined.UndefinedNoneOr[int] = undefined.UNDEFINED
+        if "width" in payload:
+            width = payload.get("width")
+
+        content_type: undefined.UndefinedNoneOr[str] = undefined.UNDEFINED
+        if "content_type" in payload:
+            content_type = payload.get("content_type")
+
+        loading_state: undefined.UndefinedNoneOr[component_models.MediaLoadingType] = undefined.UNDEFINED
+        if "loading_state" in payload:
+            if state := payload.get("loading_state"):
+                loading_state = component_models.MediaLoadingType(state)
+            else:
+                loading_state = None
+
+        return component_models.MediaResource(
+            resource=files.ensure_resource(payload["url"]),
+            proxy_resource=files.ensure_resource(payload["proxy_url"]) if "proxy_url" in payload else None,
+            height=height,
+            width=width,
+            content_type=content_type,
+            loading_state=loading_state,
+        )
+
+    def _deserialize_action_row_component(
+        self, payload: data_binding.JSONObject
+    ) -> component_models.ActionRowComponent[component_models.PartialComponent]:
+        components: list[component_models.PartialComponent] = []
+
+        for component_payload in payload["components"]:
+            component_type = component_models.ComponentType(component_payload["type"])
+
+            if (deserializer := self._action_row_component_type_mapping.get(component_type)) is None:
+                _LOGGER.debug("Unknown component type %s", component_type)
+                continue
+
+            components.append(deserializer(component_payload))
+
+        return component_models.ActionRowComponent(
+            type=component_models.ComponentType.ACTION_ROW, id=payload["id"], components=components
+        )
+
+    def _deserialize_section_component(self, payload: data_binding.JSONObject) -> component_models.SectionComponent:
+        accessory_payload = payload["accessory"]
+        accessory_type = component_models.ComponentType(accessory_payload["type"])
+        if (accessory_deserializer := self._section_accessory_mapping.get(accessory_type)) is None:
+            _LOGGER.debug("Unknown section accessory type %s", accessory_type)
+            msg = f"Unknown section accessory type {accessory_type}"
+            raise errors.UnrecognisedEntityError(msg)
+        accessory = accessory_deserializer(accessory_payload)
+
+        components: list[component_models.SectionComponentTypesT] = []
+        for component_payload in payload["components"]:
+            component_type = component_models.ComponentType(component_payload["type"])
+
+            if (deserializer := self._section_component_mapping.get(component_type)) is None:
+                _LOGGER.debug("Unknown section component with type %s", accessory_type)
+                continue
+
+            components.append(deserializer(component_payload))
+
+        return component_models.SectionComponent(
+            type=component_models.ComponentType.SECTION, id=payload["id"], components=components, accessory=accessory
+        )
+
+    def _deserialize_thumbnail_component(self, payload: data_binding.JSONObject) -> component_models.ThumbnailComponent:
+        return component_models.ThumbnailComponent(
+            type=component_models.ComponentType.THUMBNAIL,
+            id=payload["id"],
+            media=self._deserialize_media(payload["media"]),
+            description=payload.get("description", None),
+            is_spoiler=payload.get("spoiler", False),
+        )
+
+    def _deserialize_text_display_component(
+        self, payload: data_binding.JSONObject
+    ) -> component_models.TextDisplayComponent:
+        return component_models.TextDisplayComponent(
+            type=component_models.ComponentType.TEXT_DISPLAY, id=payload["id"], content=payload["content"]
+        )
+
+    def _deserialize_media_gallery_component(
+        self, payload: data_binding.JSONObject
+    ) -> component_models.MediaGalleryComponent:
+        return component_models.MediaGalleryComponent(
+            type=component_models.ComponentType.MEDIA_GALLERY,
+            id=payload["id"],
+            items=[self._deserialize_media_gallery_item(item) for item in payload["items"]],
+        )
+
+    def _deserialize_media_gallery_item(self, payload: data_binding.JSONObject) -> component_models.MediaGalleryItem:
+        return component_models.MediaGalleryItem(
+            media=self._deserialize_media(payload["media"]),
+            description=payload.get("description"),
+            is_spoiler=payload.get("spoiler", False),
+        )
+
+    def _deserialize_separator_component(self, payload: data_binding.JSONObject) -> component_models.SeparatorComponent:
+        return component_models.SeparatorComponent(
+            type=component_models.ComponentType.SEPARATOR,
+            id=payload["id"],
+            spacing=component_models.SpacingType(payload["spacing"]),
+            divider=payload.get("divider", False),
+        )
+
+    def _deserialize_file_component(self, payload: data_binding.JSONObject) -> component_models.FileComponent:
+        return component_models.FileComponent(
+            type=component_models.ComponentType.FILE,
+            id=payload["id"],
+            file=self._deserialize_media(payload["file"]),
+            is_spoiler=payload.get("spoiler", False),
+        )
+
+    def _deserialize_container_component(self, payload: data_binding.JSONObject) -> component_models.ContainerComponent:
+        components: list[component_models.ContainerTypesT] = []
+
+        for component_payload in payload["components"]:
+            component_type = component_models.ComponentType(component_payload["type"])
+
+            if component_type == component_models.ComponentType.ACTION_ROW:
+                if action_row := self._deserialize_action_row_component(component_payload):
+                    components.append(action_row)
+
+                continue
+
+            if (deserializer := self._container_component_mapping.get(component_type)) is None:
+                _LOGGER.debug("Unknown component type %s", component_type)
+                continue
+
+            components.append(deserializer(component_payload))
+
+        accent_color: color_models.Color | None = None
+        if raw_accent_color := payload.get("accent_color"):
+            accent_color = color_models.Color.from_int(raw_accent_color)
+
+        return component_models.ContainerComponent(
+            type=component_models.ComponentType.CONTAINER,
+            id=payload.get("id", None),
+            accent_color=accent_color,
+            is_spoiler=payload.get("spoiler", False),
+            components=components,
         )
 
     ##################
@@ -3365,9 +3558,11 @@ def deserialize_partial_message(  # noqa: C901, PLR0912, PLR0915
         if raw_application_id := payload.get("application_id"):
             application_id = snowflakes.Snowflake(raw_application_id)
 
-        components: undefined.UndefinedOr[list[component_models.MessageActionRowComponent]] = undefined.UNDEFINED
+        components: undefined.UndefinedOr[typing.Sequence[component_models.TopLevelComponentTypesT]] = (
+            undefined.UNDEFINED
+        )
         if component_payloads := payload.get("components"):
-            components = self._deserialize_components(component_payloads, self._message_component_type_mapping)
+            components = self._deserialize_top_level_components(component_payloads)
 
         channel_mentions: undefined.UndefinedOr[dict[snowflakes.Snowflake, channel_models.PartialChannel]] = (
             undefined.UNDEFINED
@@ -3477,10 +3672,9 @@ def deserialize_message(self, payload: data_binding.JSONObject) -> message_model
         if thread_payload := payload.get("thread"):
             thread = self.deserialize_guild_thread(thread_payload)
 
-        components: list[component_models.MessageActionRowComponent]
+        components: typing.Sequence[component_models.TopLevelComponentTypesT]
         if component_payloads := payload.get("components"):
-            components = self._deserialize_components(component_payloads, self._message_component_type_mapping)
-
+            components = self._deserialize_top_level_components(component_payloads)
         else:
             components = []
 
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index 86584730..0866458f 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -49,6 +49,7 @@
 from hikari import channels as channels_
 from hikari import colors
 from hikari import commands
+from hikari import components as components_
 from hikari import embeds as embeds_
 from hikari import emojis
 from hikari import errors
@@ -117,6 +118,17 @@
 _X_RATELIMIT_SCOPE_HEADER: typing.Final[str] = sys.intern("X-RateLimit-Scope")
 _RETRY_ERROR_CODES: typing.Final[frozenset[int]] = frozenset((500, 502, 503, 504))
 _MAX_BACKOFF_DURATION: typing.Final[int] = 16
+_V2_COMPONENT_TYPES: typing.Final[frozenset[components_.ComponentType]] = frozenset(
+    (
+        components_.ComponentType.SECTION,
+        components_.ComponentType.TEXT_DISPLAY,
+        components_.ComponentType.THUMBNAIL,
+        components_.ComponentType.MEDIA_GALLERY,
+        components_.ComponentType.FILE,
+        components_.ComponentType.SEPARATOR,
+        components_.ComponentType.CONTAINER,
+    )
+)
 
 
 class ClientCredentialsStrategy(rest_api.TokenStrategy):
@@ -1489,10 +1501,30 @@ def _build_message_payload(  # noqa: C901, PLR0912, PLR0915
 
         serialized_components: undefined.UndefinedOr[list[data_binding.JSONObject]] = undefined.UNDEFINED
         if component is not undefined.UNDEFINED:
-            serialized_components = [component.build()] if component is not None else []
+            if component is not None:
+                component_payload, component_attachments = component.build()
+                serialized_components = [component_payload]
+                final_attachments.extend(component_attachments)
+
+                if component.type in _V2_COMPONENT_TYPES:
+                    if flags is undefined.UNDEFINED:
+                        flags = 0
+                    flags |= messages_.MessageFlag.IS_COMPONENTS_V2
+            else:
+                serialized_components = []
 
         elif components is not undefined.UNDEFINED:
-            serialized_components = [component.build() for component in components] if components is not None else []
+            serialized_components = []
+            if components is not None:
+                for comp in components:
+                    component_payload, component_attachments = comp.build()
+                    serialized_components.append(component_payload)
+                    final_attachments.extend(component_attachments)
+
+                    if comp.type in _V2_COMPONENT_TYPES:
+                        if flags is undefined.UNDEFINED:
+                            flags = 0
+                        flags |= messages_.MessageFlag.IS_COMPONENTS_V2
 
         serialized_embeds: undefined.UndefinedOr[data_binding.JSONArray] = undefined.UNDEFINED
         if embed is not undefined.UNDEFINED:
@@ -1977,6 +2009,7 @@ async def execute_webhook(
 
         query = data_binding.StringMapBuilder()
         query.put("wait", True)
+        query.put("with_components", True)
         query.put("thread_id", thread)
 
         body, form_builder = self._build_message_payload(
@@ -2055,6 +2088,7 @@ async def edit_webhook_message(
         webhook_id = webhook if isinstance(webhook, int) else webhook.webhook_id
         route = routes.PATCH_WEBHOOK_MESSAGE.compile(webhook=webhook_id, token=token, message=message)
         query = data_binding.StringMapBuilder()
+        query.put("with_components", True)
         query.put("thread_id", thread)
 
         body, form_builder = self._build_message_payload(
diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py
index 1e33d829..097cacd8 100644
--- a/hikari/impl/special_endpoints.py
+++ b/hikari/impl/special_endpoints.py
@@ -29,22 +29,30 @@
     "AutocompleteChoiceBuilder",
     "ChannelSelectMenuBuilder",
     "CommandBuilder",
+    "ContainerComponentBuilder",
     "ContextMenuCommandBuilder",
+    "FileComponentBuilder",
     "InteractionAutocompleteBuilder",
     "InteractionDeferredBuilder",
     "InteractionMessageBuilder",
     "InteractionModalBuilder",
     "InteractiveButtonBuilder",
     "LinkButtonBuilder",
+    "MediaGalleryComponentBuilder",
+    "MediaGalleryItemBuilder",
     "MessageActionRowBuilder",
     "ModalActionRowBuilder",
     "PollAnswerBuilder",
     "PollBuilder",
+    "SectionComponentBuilder",
     "SelectMenuBuilder",
     "SelectOptionBuilder",
+    "SeparatorComponentBuilder",
     "SlashCommandBuilder",
+    "TextDisplayComponentBuilder",
     "TextInputBuilder",
     "TextSelectMenuBuilder",
+    "ThumbnailComponentBuilder",
     "TypingIndicator",
 )
 
@@ -77,7 +85,6 @@
     # This is insanely hacky, but it is needed for ruff to not complain until it gets type inference
     from hikari.internal import typing_extensions
 
-
 if typing.TYPE_CHECKING:
     import types
 
@@ -86,6 +93,7 @@
 
     from hikari import applications
     from hikari import audit_logs
+    from hikari import colors
     from hikari import embeds as embeds_
     from hikari import guilds
     from hikari import permissions as permissions_
@@ -1308,6 +1316,7 @@ def _build_emoji(
 @attrs_extensions.with_copy
 @attrs.define(kw_only=True, weakref_slot=False)
 class _ButtonBuilder(special_endpoints.ButtonBuilder):
+    _id: undefined.UndefinedOr[int] = attrs.field(alias="id", default=undefined.UNDEFINED)
     _style: int | component_models.ButtonStyle = attrs.field(alias="style")
     _custom_id: undefined.UndefinedOr[str] = attrs.field()
     _url: undefined.UndefinedOr[str] = attrs.field()
@@ -1327,6 +1336,11 @@ def __attrs_post_init__(self) -> None:
     def type(self) -> typing.Literal[component_models.ComponentType.BUTTON]:
         return component_models.ComponentType.BUTTON
 
+    @property
+    @typing_extensions.override
+    def id(self) -> undefined.UndefinedOr[int]:
+        return self._id
+
     @property
     @typing_extensions.override
     def style(self) -> int | component_models.ButtonStyle:
@@ -1364,12 +1378,15 @@ def set_is_disabled(self, state: bool, /) -> Self:
         return self
 
     @typing_extensions.override
-    def build(self) -> typing.MutableMapping[str, typing.Any]:
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
         data = data_binding.JSONObjectBuilder()
 
         data["type"] = component_models.ComponentType.BUTTON
         data["style"] = self._style
         data["disabled"] = self._is_disabled
+        data.put("id", self._id)
         data.put("label", self._label)
 
         if self._emoji_id is not undefined.UNDEFINED:
@@ -1381,7 +1398,7 @@ def build(self) -> typing.MutableMapping[str, typing.Any]:
         data.put("custom_id", self._custom_id)
         data.put("url", self._url)
 
-        return data
+        return data, []
 
 
 @attrs.define(kw_only=True, weakref_slot=False)
@@ -1512,6 +1529,7 @@ def build(self) -> typing.MutableMapping[str, typing.Any]:
 class SelectMenuBuilder(special_endpoints.SelectMenuBuilder):
     """Builder class for select menus."""
 
+    _id: undefined.UndefinedOr[int] = attrs.field(alias="id", default=undefined.UNDEFINED)
     _type: component_models.ComponentType | int = attrs.field(alias="type")
     _custom_id: str = attrs.field(alias="custom_id")
     _placeholder: undefined.UndefinedOr[str] = attrs.field(alias="placeholder", default=undefined.UNDEFINED)
@@ -1524,6 +1542,11 @@ class SelectMenuBuilder(special_endpoints.SelectMenuBuilder):
     def type(self) -> int | component_models.ComponentType:
         return self._type
 
+    @property
+    @typing_extensions.override
+    def id(self) -> undefined.UndefinedOr[int]:
+        return self._id
+
     @property
     @typing_extensions.override
     def custom_id(self) -> str:
@@ -1575,16 +1598,19 @@ def set_max_values(self, value: int, /) -> Self:
         return self
 
     @typing_extensions.override
-    def build(self) -> typing.MutableMapping[str, typing.Any]:
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
         data = data_binding.JSONObjectBuilder()
 
         data["type"] = self._type
         data["custom_id"] = self._custom_id
+        data.put("id", self._id)
         data.put("placeholder", self._placeholder)
         data.put("min_values", self._min_values)
         data.put("max_values", self._max_values)
         data.put("disabled", self._is_disabled)
-        return data
+        return data, []
 
 
 @attrs.define(init=False, weakref_slot=False)
@@ -1604,6 +1630,7 @@ class TextSelectMenuBuilder(SelectMenuBuilder, special_endpoints.TextSelectMenuB
     def __init__(
         self,
         *,
+        id: undefined.UndefinedOr[int] = undefined.UNDEFINED,
         custom_id: str,
         parent: _ParentT,
         options: typing.Sequence[special_endpoints.SelectOptionBuilder] = (),
@@ -1617,6 +1644,7 @@ def __init__(
     def __init__(
         self: TextSelectMenuBuilder[typing.NoReturn],
         *,
+        id: undefined.UndefinedOr[int] = undefined.UNDEFINED,
         custom_id: str,
         options: typing.Sequence[special_endpoints.SelectOptionBuilder] = (),
         placeholder: undefined.UndefinedOr[str] = undefined.UNDEFINED,
@@ -1628,6 +1656,7 @@ def __init__(
     def __init__(
         self,
         *,
+        id: undefined.UndefinedOr[int] = undefined.UNDEFINED,
         custom_id: str,
         parent: _ParentT | None = None,
         options: typing.Sequence[special_endpoints.SelectOptionBuilder] = (),
@@ -1638,6 +1667,7 @@ def __init__(
     ) -> None:
         super().__init__(
             type=component_models.ComponentType.TEXT_SELECT_MENU,
+            id=id,
             custom_id=custom_id,
             placeholder=placeholder,
             min_values=min_values,
@@ -1681,11 +1711,13 @@ def add_raw_option(self, option: special_endpoints.SelectOptionBuilder, /) -> Se
         return self
 
     @typing_extensions.override
-    def build(self) -> typing.MutableMapping[str, typing.Any]:
-        data = super().build()
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
+        payload, attachments = super().build()
 
-        data["options"] = [option.build() for option in self._options]
-        return data
+        payload["options"] = [option.build() for option in self._options]
+        return payload, attachments
 
 
 @attrs_extensions.with_copy
@@ -1709,11 +1741,13 @@ def set_channel_types(self, value: typing.Sequence[channels.ChannelType], /) ->
         return self
 
     @typing_extensions.override
-    def build(self) -> typing.MutableMapping[str, typing.Any]:
-        data = super().build()
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
+        payload, attachments = super().build()
 
-        data["channel_types"] = self._channel_types
-        return data
+        payload["channel_types"] = self._channel_types
+        return payload, attachments
 
 
 @attrs_extensions.with_copy
@@ -1721,6 +1755,7 @@ def build(self) -> typing.MutableMapping[str, typing.Any]:
 class TextInputBuilder(special_endpoints.TextInputBuilder):
     """Standard implementation of [`hikari.api.special_endpoints.TextInputBuilder`][]."""
 
+    _id: undefined.UndefinedOr[int] = attrs.field(alias="id", default=undefined.UNDEFINED)
     _custom_id: str = attrs.field(alias="custom_id")
     _label: str = attrs.field(alias="label")
 
@@ -1738,6 +1773,11 @@ class TextInputBuilder(special_endpoints.TextInputBuilder):
     def type(self) -> typing.Literal[component_models.ComponentType.TEXT_INPUT]:
         return component_models.ComponentType.TEXT_INPUT
 
+    @property
+    @typing_extensions.override
+    def id(self) -> undefined.UndefinedOr[int]:
+        return self._id
+
     @property
     @typing_extensions.override
     def custom_id(self) -> str:
@@ -1819,27 +1859,33 @@ def set_max_length(self, max_length: int, /) -> Self:
         return self
 
     @typing_extensions.override
-    def build(self) -> typing.MutableMapping[str, typing.Any]:
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
         data = data_binding.JSONObjectBuilder()
 
         data["type"] = component_models.ComponentType.TEXT_INPUT
         data["style"] = self._style
         data["custom_id"] = self._custom_id
         data["label"] = self._label
+        data.put("id", self._id)
         data.put("placeholder", self._placeholder)
         data.put("value", self._value)
         data.put("required", self._required)
         data.put("min_length", self._min_length)
         data.put("max_length", self._max_length)
 
-        return data
+        return data, []
 
 
 @attrs.define(kw_only=True, weakref_slot=False)
 class MessageActionRowBuilder(special_endpoints.MessageActionRowBuilder):
     """Standard implementation of [`hikari.api.special_endpoints.MessageActionRowBuilder`][]."""
 
-    _components: list[special_endpoints.ComponentBuilder] = attrs.field(alias="components", factory=list)
+    _id: undefined.UndefinedOr[int] = attrs.field(alias="id", default=undefined.UNDEFINED)
+    _components: list[special_endpoints.MessageActionRowBuilderComponentsT] = attrs.field(
+        alias="components", factory=list
+    )
     _stored_type: int | None = attrs.field(default=None, init=False)
 
     @property
@@ -1849,7 +1895,12 @@ def type(self) -> typing.Literal[component_models.ComponentType.ACTION_ROW]:
 
     @property
     @typing_extensions.override
-    def components(self) -> typing.Sequence[special_endpoints.ComponentBuilder]:
+    def id(self) -> undefined.UndefinedOr[int]:
+        return self._id
+
+    @property
+    @typing_extensions.override
+    def components(self) -> typing.Sequence[special_endpoints.MessageActionRowBuilderComponentsT]:
         return self._components.copy()
 
     def _assert_can_add_type(self, type_: component_models.ComponentType | int, /) -> None:
@@ -1860,7 +1911,7 @@ def _assert_can_add_type(self, type_: component_models.ComponentType | int, /) -
         self._stored_type = type_
 
     @typing_extensions.override
-    def add_component(self, component: special_endpoints.ComponentBuilder, /) -> Self:
+    def add_component(self, component: special_endpoints.MessageActionRowBuilderComponentsT, /) -> Self:
         self._assert_can_add_type(component.type)
         self._components.append(component)
         return self
@@ -1875,10 +1926,11 @@ def add_interactive_button(
         emoji: snowflakes.Snowflakeish | emojis.Emoji | str | undefined.UndefinedType = undefined.UNDEFINED,
         label: undefined.UndefinedOr[str] = undefined.UNDEFINED,
         is_disabled: bool = False,
+        id: undefined.UndefinedOr[int] = undefined.UNDEFINED,
     ) -> Self:
         return self.add_component(
             InteractiveButtonBuilder(
-                style=style, custom_id=custom_id, emoji=emoji, label=label, is_disabled=is_disabled
+                id=id, style=style, custom_id=custom_id, emoji=emoji, label=label, is_disabled=is_disabled
             )
         )
 
@@ -1891,8 +1943,9 @@ def add_link_button(
         emoji: snowflakes.Snowflakeish | emojis.Emoji | str | undefined.UndefinedType = undefined.UNDEFINED,
         label: undefined.UndefinedOr[str] = undefined.UNDEFINED,
         is_disabled: bool = False,
+        id: undefined.UndefinedOr[int] = undefined.UNDEFINED,
     ) -> Self:
-        return self.add_component(LinkButtonBuilder(url=url, label=label, emoji=emoji, is_disabled=is_disabled))
+        return self.add_component(LinkButtonBuilder(id=id, url=url, label=label, emoji=emoji, is_disabled=is_disabled))
 
     @typing_extensions.override
     def add_select_menu(
@@ -1905,10 +1958,12 @@ def add_select_menu(
         min_values: int = 0,
         max_values: int = 1,
         is_disabled: bool = False,
+        id: undefined.UndefinedOr[int] = undefined.UNDEFINED,
     ) -> Self:
         return self.add_component(
             SelectMenuBuilder(
                 type=type_,
+                id=id,
                 custom_id=custom_id,
                 placeholder=placeholder,
                 min_values=min_values,
@@ -1928,9 +1983,11 @@ def add_channel_menu(
         min_values: int = 0,
         max_values: int = 1,
         is_disabled: bool = False,
+        id: undefined.UndefinedOr[int] = undefined.UNDEFINED,
     ) -> Self:
         return self.add_component(
             ChannelSelectMenuBuilder(
+                id=id,
                 custom_id=custom_id,
                 placeholder=placeholder,
                 channel_types=channel_types,
@@ -1950,8 +2007,10 @@ def add_text_menu(
         min_values: int = 0,
         max_values: int = 1,
         is_disabled: bool = False,
+        id: undefined.UndefinedOr[int] = undefined.UNDEFINED,
     ) -> special_endpoints.TextSelectMenuBuilder[Self]:
         component = TextSelectMenuBuilder(
+            id=id,
             custom_id=custom_id,
             parent=self,
             placeholder=placeholder,
@@ -1963,18 +2022,34 @@ def add_text_menu(
         return component
 
     @typing_extensions.override
-    def build(self) -> typing.MutableMapping[str, typing.Any]:
-        return {
-            "type": component_models.ComponentType.ACTION_ROW,
-            "components": [component.build() for component in self._components],
-        }
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
+        payload = data_binding.JSONObjectBuilder()
+
+        payload.put("id", self._id)
+        payload.put("type", component_models.ComponentType.ACTION_ROW)
+
+        components_payload: typing.MutableSequence[typing.Any] = []
+        attachments: typing.MutableSequence[files.Resource[files.AsyncReader]] = []
+        for component in self.components:
+            component_payload, component_attachments = component.build()
+            components_payload.append(component_payload)
+            attachments.extend(component_attachments)
+
+        payload.put_array("components", components_payload)
+
+        return payload, attachments
 
 
 @attrs.define(kw_only=True, weakref_slot=False)
 class ModalActionRowBuilder(special_endpoints.ModalActionRowBuilder):
     """Standard implementation of [`hikari.api.special_endpoints.ModalActionRowBuilder`][]."""
 
-    _components: list[special_endpoints.ComponentBuilder] = attrs.field(alias="components", factory=list)
+    _id: undefined.UndefinedOr[int] = attrs.field(alias="id", default=undefined.UNDEFINED)
+    _components: list[special_endpoints.ModalActionRowBuilderComponentsT] = attrs.field(
+        alias="components", factory=list
+    )
     _stored_type: int | None = attrs.field(init=False, default=None)
 
     @property
@@ -1984,7 +2059,12 @@ def type(self) -> typing.Literal[component_models.ComponentType.ACTION_ROW]:
 
     @property
     @typing_extensions.override
-    def components(self) -> typing.Sequence[special_endpoints.ComponentBuilder]:
+    def id(self) -> undefined.UndefinedOr[int]:
+        return self._id
+
+    @property
+    @typing_extensions.override
+    def components(self) -> typing.Sequence[special_endpoints.ModalActionRowBuilderComponentsT]:
         return self._components.copy()
 
     def _assert_can_add_type(self, type_: component_models.ComponentType | int, /) -> None:
@@ -1995,7 +2075,7 @@ def _assert_can_add_type(self, type_: component_models.ComponentType | int, /) -
         self._stored_type = type_
 
     @typing_extensions.override
-    def add_component(self, component: special_endpoints.ComponentBuilder, /) -> Self:
+    def add_component(self, component: special_endpoints.ModalActionRowBuilderComponentsT, /) -> Self:
         self._assert_can_add_type(component.type)
         self._components.append(component)
         return self
@@ -2028,11 +2108,466 @@ def add_text_input(
         )
 
     @typing_extensions.override
-    def build(self) -> typing.MutableMapping[str, typing.Any]:
-        return {
-            "type": component_models.ComponentType.ACTION_ROW,
-            "components": [component.build() for component in self._components],
-        }
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
+        component_payloads: list[typing.MutableMapping[str, typing.Any]] = []
+        attachments: list[files.Resource[files.AsyncReader]] = []
+
+        for component in self.components:
+            component_payload, component_attachments = component.build()
+            component_payloads.append(component_payload)
+            attachments.extend(component_attachments)
+
+        payload = data_binding.JSONObjectBuilder()
+        payload["type"] = self.type
+        payload["components"] = component_payloads
+        payload.put("id", self._id)
+
+        return payload, attachments
+
+
+def _build_media_resource(resource: files.Resource[files.AsyncReader]) -> dict[str, typing.Any]:
+    return {"url": resource.url}
+
+
+@attrs.define(kw_only=True, weakref_slot=False)
+class SectionComponentBuilder(special_endpoints.SectionComponentBuilder):
+    """Standard implementation of [`hikari.api.special_endpoints.SectionComponentBuilder`][]."""
+
+    _id: undefined.UndefinedOr[int] = attrs.field(alias="id", default=undefined.UNDEFINED)
+    _components: list[special_endpoints.SectionBuilderComponentsT] = attrs.field(alias="components", factory=list)
+    _accessory: special_endpoints.SectionBuilderAccessoriesT = attrs.field(alias="accessory")
+
+    @property
+    @typing_extensions.override
+    def type(self) -> typing.Literal[component_models.ComponentType.SECTION]:
+        return component_models.ComponentType.SECTION
+
+    @property
+    @typing_extensions.override
+    def id(self) -> undefined.UndefinedOr[int]:
+        return self._id
+
+    @property
+    @typing_extensions.override
+    def components(self) -> typing.Sequence[special_endpoints.SectionBuilderComponentsT]:
+        return self._components.copy()
+
+    @property
+    @typing_extensions.override
+    def accessory(self) -> special_endpoints.SectionBuilderAccessoriesT:
+        return self._accessory
+
+    @typing_extensions.override
+    def add_component(self, component: special_endpoints.SectionBuilderComponentsT) -> Self:
+        self._components.append(component)
+        return self
+
+    @typing_extensions.override
+    def add_text_display(self, content: str, *, id: undefined.UndefinedOr[int] = undefined.UNDEFINED) -> Self:
+        component = TextDisplayComponentBuilder(id=id, content=content)
+        self.add_component(component)
+        return self
+
+    @typing_extensions.override
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
+        payload = data_binding.JSONObjectBuilder()
+        payload["type"] = self.type
+        payload.put("id", self._id)
+
+        accessory_payload, accessory_attachments = self.accessory.build()
+
+        attachments: list[files.Resource[files.AsyncReader]] = list(accessory_attachments)
+        components_payload: list[typing.Mapping[str, typing.Any]] = []
+        for component in self.components:
+            component_payload, component_attachments = component.build()
+
+            components_payload.append(component_payload)
+            attachments.extend(component_attachments)
+
+        payload["components"] = components_payload
+        payload["accessory"] = accessory_payload
+
+        return payload, attachments
+
+
+@attrs.define(kw_only=True, weakref_slot=False)
+class TextDisplayComponentBuilder(special_endpoints.TextDisplayComponentBuilder):
+    """Standard implementation of [`hikari.api.special_endpoints.TextDisplayComponentBuilder`][]."""
+
+    _id: undefined.UndefinedOr[int] = attrs.field(alias="id", default=undefined.UNDEFINED)
+    _content: str = attrs.field(alias="content")
+
+    @property
+    @typing_extensions.override
+    def type(self) -> typing.Literal[component_models.ComponentType.TEXT_DISPLAY]:
+        return component_models.ComponentType.TEXT_DISPLAY
+
+    @property
+    @typing_extensions.override
+    def id(self) -> undefined.UndefinedOr[int]:
+        return self._id
+
+    @property
+    @typing_extensions.override
+    def content(self) -> str:
+        return self._content
+
+    @typing_extensions.override
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
+        payload = data_binding.JSONObjectBuilder()
+        payload["type"] = self.type
+        payload["content"] = self._content
+
+        payload.put("id", self._id)
+
+        return payload, ()
+
+
+@attrs.define(kw_only=True, weakref_slot=False)
+class ThumbnailComponentBuilder(special_endpoints.ThumbnailComponentBuilder):
+    """Standard implementation of [`hikari.api.special_endpoints.ThumbnailComponentBuilder`][]."""
+
+    _id: undefined.UndefinedOr[int] = attrs.field(alias="id", default=undefined.UNDEFINED)
+    _media: files.Resourceish = attrs.field(alias="media")
+    _description: undefined.UndefinedOr[str] = attrs.field(alias="description", default=undefined.UNDEFINED)
+    _spoiler: bool = attrs.field(alias="spoiler", default=False)
+
+    @property
+    @typing_extensions.override
+    def type(self) -> typing.Literal[component_models.ComponentType.THUMBNAIL]:
+        return component_models.ComponentType.THUMBNAIL
+
+    @property
+    @typing_extensions.override
+    def id(self) -> undefined.UndefinedOr[int]:
+        return self._id
+
+    @property
+    @typing_extensions.override
+    def media(self) -> files.Resourceish:
+        return self._media
+
+    @property
+    @typing_extensions.override
+    def description(self) -> undefined.UndefinedOr[str]:
+        return self._description
+
+    @property
+    @typing_extensions.override
+    def is_spoiler(self) -> bool:
+        return self._spoiler
+
+    @typing_extensions.override
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
+        media_resource = files.ensure_resource(self._media)
+
+        payload = data_binding.JSONObjectBuilder()
+        payload["type"] = self.type
+        payload["media"] = _build_media_resource(media_resource)
+        payload["spoiler"] = self._spoiler
+
+        payload.put("id", self._id)
+        payload.put("description", self._description)
+
+        return payload, (media_resource,)
+
+
+@attrs.define(kw_only=True, weakref_slot=False)
+class MediaGalleryComponentBuilder(special_endpoints.MediaGalleryComponentBuilder):
+    """Standard implementation of [`hikari.api.special_endpoints.MediaGalleryComponentBuilder`][]."""
+
+    _id: undefined.UndefinedOr[int] = attrs.field(alias="id", default=undefined.UNDEFINED)
+    _items: list[special_endpoints.MediaGalleryItemBuilder] = attrs.field(alias="items", factory=list)
+
+    @property
+    @typing_extensions.override
+    def type(self) -> typing.Literal[component_models.ComponentType.MEDIA_GALLERY]:
+        return component_models.ComponentType.MEDIA_GALLERY
+
+    @property
+    @typing_extensions.override
+    def id(self) -> undefined.UndefinedOr[int]:
+        return self._id
+
+    @property
+    @typing_extensions.override
+    def items(self) -> typing.Sequence[special_endpoints.MediaGalleryItemBuilder]:
+        return self._items.copy()
+
+    @typing_extensions.override
+    def add_item(self, item: special_endpoints.MediaGalleryItemBuilder) -> Self:
+        self._items.append(item)
+        return self
+
+    @typing_extensions.override
+    def add_media_gallery_item(
+        self,
+        media: files.Resourceish,
+        *,
+        description: undefined.UndefinedOr[str] = undefined.UNDEFINED,
+        spoiler: bool = False,
+    ) -> Self:
+        self.add_item(MediaGalleryItemBuilder(media=media, description=description, spoiler=spoiler))
+        return self
+
+    @typing_extensions.override
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
+        items_payload: list[typing.MutableMapping[str, typing.Any]] = []
+        attachments: list[files.Resource[files.AsyncReader]] = []
+        for item in self.items:
+            item_payload, item_attachments = item.build()
+            items_payload.append(item_payload)
+            attachments.extend(item_attachments)
+
+        payload = data_binding.JSONObjectBuilder()
+        payload["type"] = self.type
+        payload["items"] = items_payload
+        payload.put("id", self._id)
+
+        return payload, attachments
+
+
+@attrs.define(kw_only=True, weakref_slot=False)
+class MediaGalleryItemBuilder(special_endpoints.MediaGalleryItemBuilder):
+    """Standard implementation of [`hikari.api.special_endpoints.MediaGalleryItemBuilder`][]."""
+
+    _media: files.Resourceish = attrs.field(alias="media")
+    _description: undefined.UndefinedOr[str] = attrs.field(alias="description", default=undefined.UNDEFINED)
+    _spoiler: bool = attrs.field(alias="spoiler", default=False)
+
+    @property
+    @typing_extensions.override
+    def media(self) -> files.Resourceish:
+        return self._media
+
+    @property
+    @typing_extensions.override
+    def description(self) -> undefined.UndefinedOr[str]:
+        return self._description
+
+    @property
+    @typing_extensions.override
+    def is_spoiler(self) -> bool:
+        return self._spoiler
+
+    @typing_extensions.override
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
+        media_resource = files.ensure_resource(self._media)
+
+        payload = data_binding.JSONObjectBuilder()
+        payload["media"] = _build_media_resource(media_resource)
+        payload["spoiler"] = self._spoiler
+        payload.put("description", self._description)
+
+        return payload, (media_resource,)
+
+
+@attrs.define(kw_only=True, weakref_slot=False)
+class SeparatorComponentBuilder(special_endpoints.SeparatorComponentBuilder):
+    """Standard implementation of [`hikari.api.special_endpoints.SeparatorComponentBuilder`][]."""
+
+    _id: undefined.UndefinedOr[int] = attrs.field(alias="id", default=undefined.UNDEFINED)
+    _spacing: undefined.UndefinedOr[component_models.SpacingType] = attrs.field(
+        alias="spacing", default=undefined.UNDEFINED
+    )
+    _divider: undefined.UndefinedOr[bool] = attrs.field(alias="divider", default=undefined.UNDEFINED)
+
+    @property
+    @typing_extensions.override
+    def type(self) -> typing.Literal[component_models.ComponentType.SEPARATOR]:
+        return component_models.ComponentType.SEPARATOR
+
+    @property
+    @typing_extensions.override
+    def id(self) -> undefined.UndefinedOr[int]:
+        return self._id
+
+    @property
+    @typing_extensions.override
+    def spacing(self) -> undefined.UndefinedOr[component_models.SpacingType]:
+        return self._spacing
+
+    @property
+    @typing_extensions.override
+    def divider(self) -> undefined.UndefinedOr[bool]:
+        return self._divider
+
+    @typing_extensions.override
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
+        payload = data_binding.JSONObjectBuilder()
+        payload["type"] = self.type
+        payload.put("id", self._id)
+        payload.put("spacing", self._spacing)
+        payload.put("divider", self._divider)
+
+        return payload, ()
+
+
+@attrs.define(kw_only=True, weakref_slot=False)
+class FileComponentBuilder(special_endpoints.FileComponentBuilder):
+    """Standard implementation of [`hikari.api.special_endpoints.FileComponentBuilder`][]."""
+
+    _id: undefined.UndefinedOr[int] = attrs.field(alias="id", default=undefined.UNDEFINED)
+    _file: files.Resourceish = attrs.field(alias="file")
+    _spoiler: bool = attrs.field(alias="spoiler", default=False)
+
+    @property
+    @typing_extensions.override
+    def type(self) -> typing.Literal[component_models.ComponentType.FILE]:
+        return component_models.ComponentType.FILE
+
+    @property
+    @typing_extensions.override
+    def id(self) -> undefined.UndefinedOr[int]:
+        return self._id
+
+    @property
+    @typing_extensions.override
+    def file(self) -> files.Resourceish:
+        return self._file
+
+    @property
+    @typing_extensions.override
+    def is_spoiler(self) -> bool:
+        return self._spoiler
+
+    @typing_extensions.override
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
+        file_resource = files.ensure_resource(self._file)
+
+        payload = data_binding.JSONObjectBuilder()
+        payload["type"] = self.type
+        payload["spoiler"] = self._spoiler
+        payload["file"] = _build_media_resource(file_resource)
+        payload.put("id", self._id)
+
+        return payload, (file_resource,)
+
+
+@attrs.define(kw_only=True, weakref_slot=False)
+class ContainerComponentBuilder(special_endpoints.ContainerComponentBuilder):
+    """Standard implementation of [`hikari.api.special_endpoints.ContainerComponentBuilder`][]."""
+
+    _id: undefined.UndefinedOr[int] = attrs.field(alias="id", default=undefined.UNDEFINED)
+    _accent_color: undefined.UndefinedOr[colors.Color] = attrs.field(alias="accent_color", default=undefined.UNDEFINED)
+    _spoiler: bool = attrs.field(alias="spoiler", default=False)
+
+    _components: list[special_endpoints.ContainerBuilderComponentsT] = attrs.field(alias="components", factory=list)
+
+    @property
+    @typing_extensions.override
+    def type(self) -> typing.Literal[component_models.ComponentType.CONTAINER]:
+        return component_models.ComponentType.CONTAINER
+
+    @property
+    @typing_extensions.override
+    def id(self) -> undefined.UndefinedOr[int]:
+        return self._id
+
+    @property
+    @typing_extensions.override
+    def accent_color(self) -> undefined.UndefinedOr[colors.Color]:
+        return self._accent_color
+
+    @property
+    @typing_extensions.override
+    def is_spoiler(self) -> bool:
+        return self._spoiler
+
+    @property
+    @typing_extensions.override
+    def components(self) -> typing.Sequence[special_endpoints.ContainerBuilderComponentsT]:
+        return self._components.copy()
+
+    @typing_extensions.override
+    def add_component(self, component: special_endpoints.ContainerBuilderComponentsT) -> Self:
+        self._components.append(component)
+        return self
+
+    @typing_extensions.override
+    def add_action_row(
+        self,
+        components: typing.Sequence[special_endpoints.MessageActionRowBuilderComponentsT],
+        *,
+        id: undefined.UndefinedOr[int] = undefined.UNDEFINED,
+    ) -> Self:
+        component = MessageActionRowBuilder(id=id, components=list(components))
+        self.add_component(component)
+        return self
+
+    @typing_extensions.override
+    def add_text_display(self, content: str, *, id: undefined.UndefinedOr[int] = undefined.UNDEFINED) -> Self:
+        component = TextDisplayComponentBuilder(id=id, content=content)
+        self.add_component(component)
+        return self
+
+    @typing_extensions.override
+    def add_media_gallery(
+        self,
+        items: typing.Sequence[special_endpoints.MediaGalleryItemBuilder],
+        *,
+        id: undefined.UndefinedOr[int] = undefined.UNDEFINED,
+    ) -> Self:
+        component = MediaGalleryComponentBuilder(id=id, items=list(items))
+        self.add_component(component)
+        return self
+
+    @typing_extensions.override
+    def add_separator(
+        self,
+        *,
+        spacing: undefined.UndefinedOr[component_models.SpacingType] = undefined.UNDEFINED,
+        divider: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
+        id: undefined.UndefinedOr[int] = undefined.UNDEFINED,
+    ) -> Self:
+        component = SeparatorComponentBuilder(id=id, spacing=spacing, divider=divider)
+        self.add_component(component)
+        return self
+
+    @typing_extensions.override
+    def add_file(
+        self, file: files.Resourceish, *, spoiler: bool = False, id: undefined.UndefinedOr[int] = undefined.UNDEFINED
+    ) -> Self:
+        component = FileComponentBuilder(id=id, file=file, spoiler=spoiler)
+        self.add_component(component)
+        return self
+
+    @typing_extensions.override
+    def build(
+        self,
+    ) -> tuple[typing.MutableMapping[str, typing.Any], typing.Sequence[files.Resource[files.AsyncReader]]]:
+        payload = data_binding.JSONObjectBuilder()
+        payload["type"] = self.type
+        payload["spoiler"] = self._spoiler
+        payload.put("id", self._id)
+        payload.put("accent_color", self._accent_color)
+
+        components_payload: typing.MutableSequence[typing.MutableMapping[str, typing.Any]] = []
+        attachments: typing.MutableSequence[files.Resource[files.AsyncReader]] = []
+        for component in self.components:
+            component_payload, component_attachments = component.build()
+            components_payload.append(component_payload)
+            attachments.extend(component_attachments)
+
+        payload.put_array("components", components_payload)
+
+        return payload, attachments
 
 
 @attrs.define(kw_only=True, weakref_slot=False)
diff --git a/hikari/internal/cache.py b/hikari/internal/cache.py
index 9b73d63b..8fd724f0 100644
--- a/hikari/internal/cache.py
+++ b/hikari/internal/cache.py
@@ -745,7 +745,7 @@ class MessageData(BaseData[messages.Message]):
     nonce: str | None = attrs.field()
     referenced_message: RefCell[MessageData] | None = attrs.field()
     application_id: snowflakes.Snowflake | None = attrs.field()
-    components: tuple[components_.MessageActionRowComponent, ...] = attrs.field()
+    components: tuple[components_.TopLevelComponentTypesT, ...] = attrs.field()
     thread: channels_.GuildThreadChannel | None = attrs.field()
     interaction_metadata: base_interactions.PartialInteractionMetadata | None = attrs.field()
 
diff --git a/hikari/messages.py b/hikari/messages.py
index 3f503099..83f8c0cf 100644
--- a/hikari/messages.py
+++ b/hikari/messages.py
@@ -188,6 +188,9 @@ class MessageFlag(enums.Flag):
     IS_VOICE_MESSAGE = 1 << 13
     """This message is a voice message."""
 
+    IS_COMPONENTS_V2 = 1 << 15
+    """This message uses the new components system."""
+
 
 @typing.final
 class MessageActivityType(int, enums.Enum):
@@ -600,7 +603,7 @@ class PartialMessage(snowflakes.Unique):
         This will only be provided for interaction messages.
     """
 
-    components: undefined.UndefinedOr[typing.Sequence[component_models.MessageActionRowComponent]] = attrs.field(
+    components: undefined.UndefinedOr[typing.Sequence[component_models.TopLevelComponentTypesT]] = attrs.field(
         hash=False, eq=False, repr=False
     )
     """Sequence of the components attached to this message."""
@@ -1432,7 +1435,7 @@ class Message(PartialMessage):
         This will only be provided for interaction messages.
     """
 
-    components: typing.Sequence[component_models.MessageActionRowComponent] = attrs.field(
+    components: typing.Sequence[component_models.TopLevelComponentTypesT] = attrs.field(
         hash=False, eq=False, repr=False
     )
     """Sequence of the components attached to this message."""
diff --git a/pipelines/slotscheck.nox.py b/pipelines/slotscheck.nox.py
index d9ad14a2..ae061915 100644
--- a/pipelines/slotscheck.nox.py
+++ b/pipelines/slotscheck.nox.py
@@ -30,4 +30,4 @@
 def slotscheck(session: nox.Session) -> None:
     """Check for common slotting mistakes."""
     nox.sync(session, self=True, groups=["slotscheck"])
-    session.run("slotscheck", "-m", config.MAIN_PACKAGE)
+    session.run("slotscheck", "-m", config.MAIN_PACKAGE, "--verbose")


Test patch:
diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py
index 0231df8b..89752df6 100644
--- a/tests/hikari/impl/test_entity_factory.py
+++ b/tests/hikari/impl/test_entity_factory.py
@@ -5111,10 +5111,15 @@ def modal_interaction_payload(self, interaction_member_payload, message_payload,
             "id": "846462639134605312",
             "guild_id": "290926798626357999",
             "data": {
+                "id": 0,
                 "custom_id": "modaltest",
                 "components": [
-                    {"type": 1, "components": [{"value": "Wumpus", "type": 4, "custom_id": "name"}]},
-                    {"type": 1, "components": [{"value": "Longer Text", "type": 4, "custom_id": "about"}]},
+                    {"type": 1, "id": 1, "components": [{"id": 2, "value": "Wumpus", "type": 4, "custom_id": "name"}]},
+                    {
+                        "type": 1,
+                        "id": 3,
+                        "components": [{"id": 4, "value": "Longer Text", "type": 4, "custom_id": "about"}],
+                    },
                 ],
             },
             "channel": guild_text_channel_payload,
@@ -5644,12 +5649,13 @@ def test_max_age_when_zero(self, entity_factory_impl, invite_with_metadata_paylo
 
     @pytest.fixture
     def action_row_payload(self, button_payload):
-        return {"type": 1, "components": [button_payload]}
+        return {"type": 1, "id": 8394572, "components": [button_payload]}
 
     @pytest.fixture
     def button_payload(self, custom_emoji_payload):
         return {
             "type": 2,
+            "id": 9173652,
             "label": "Click me!",
             "style": 1,
             "emoji": custom_emoji_payload,
@@ -5658,10 +5664,11 @@ def button_payload(self, custom_emoji_payload):
             "disabled": True,
         }
 
-    def test_deserialize__deserialize_button(self, entity_factory_impl, button_payload, custom_emoji_payload):
+    def test__deserialize_button(self, entity_factory_impl, button_payload, custom_emoji_payload):
         button = entity_factory_impl._deserialize_button(button_payload)
 
         assert button.type is component_models.ComponentType.BUTTON
+        assert button.id == 9173652
         assert button.style is component_models.ButtonStyle.PRIMARY
         assert button.label == "Click me!"
         assert button.emoji == entity_factory_impl.deserialize_emoji(custom_emoji_payload)
@@ -5672,9 +5679,10 @@ def test_deserialize__deserialize_button(self, entity_factory_impl, button_paylo
     def test_deserialize__deserialize_button_with_unset_fields(
         self, entity_factory_impl, button_payload, custom_emoji_payload
     ):
-        button = entity_factory_impl._deserialize_button({"type": 2, "style": 5})
+        button = entity_factory_impl._deserialize_button({"id": 0, "type": 2, "style": 5})
 
         assert button.type is component_models.ComponentType.BUTTON
+        assert button.id == 0
         assert button.style is component_models.ButtonStyle.LINK
         assert button.label is None
         assert button.emoji is None
@@ -5686,6 +5694,7 @@ def test_deserialize__deserialize_button_with_unset_fields(
     def select_menu_payload(self, custom_emoji_payload):
         return {
             "type": 5,
+            "id": 9830741,
             "custom_id": "Not an ID",
             "options": [
                 {
@@ -5706,6 +5715,7 @@ def test__deserialize_text_select_menu(self, entity_factory_impl, select_menu_pa
         menu = entity_factory_impl._deserialize_text_select_menu(select_menu_payload)
 
         assert menu.type is component_models.ComponentType.USER_SELECT_MENU
+        assert menu.id == 9830741
         assert menu.custom_id == "Not an ID"
 
         # SelectMenuOption
@@ -5725,9 +5735,11 @@ def test__deserialize_text_select_menu(self, entity_factory_impl, select_menu_pa
 
     def test__deserialize_text_select_menu_partial(self, entity_factory_impl):
         menu = entity_factory_impl._deserialize_text_select_menu(
-            {"type": 3, "custom_id": "Not an ID", "options": [{"label": "Trans", "value": "very trans"}]}
+            {"type": 3, "id": 0, "custom_id": "Not an ID", "options": [{"label": "Trans", "value": "very trans"}]}
         )
 
+        assert menu.id == 0
+
         # SelectMenuOption
         assert len(menu.options) == 1
         option = menu.options[0]
@@ -5740,6 +5752,336 @@ def test__deserialize_text_select_menu_partial(self, entity_factory_impl):
         assert menu.max_values == 1
         assert menu.is_disabled is False
 
+    @pytest.fixture
+    def text_input_payload(self):
+        return {"type": 4, "id": 3904875, "custom_id": "name", "value": "Wumpus"}
+
+    def test__deserialize_text_input(self, entity_factory_impl, text_input_payload):
+        text_input = entity_factory_impl._deserialize_text_input(text_input_payload)
+
+        assert text_input.type == component_models.ComponentType.TEXT_INPUT
+        assert text_input.id == 3904875
+        assert text_input.custom_id == "name"
+        assert text_input.value == "Wumpus"
+
+        assert isinstance(text_input, component_models.TextInputComponent)
+
+    @pytest.fixture
+    def media_payload(self):
+        return {
+            "url": "https://com.com.com.com.com.com.com.com.com.com/",
+            "proxy_url": "https://example.example.example.example.example.example.example.example.example.example/",
+            "width": 9845,
+            "height": 1904,
+            "content_type": "ooga/booga",
+            "loading_state": 2,
+        }
+
+    @pytest.fixture
+    def text_display_payload(self):
+        return {"type": 10, "id": 9840745, "content": "A text display!"}
+
+    @pytest.fixture
+    def thumbnail_payload(self, media_payload):
+        return {
+            "type": 11,
+            "id": 9824133,
+            "media": media_payload,
+            "description": "A cool description.",
+            "spoiler": False,
+        }
+
+    @pytest.fixture
+    def section_payload(self, button_payload, text_display_payload):
+        return {"type": 9, "id": 9478385, "accessory": button_payload, "components": [text_display_payload]}
+
+    @pytest.fixture
+    def media_gallery_item_payload(self, media_payload):
+        return {"media": media_payload, "description": "Gallery item description?", "spoiler": True}
+
+    @pytest.fixture
+    def media_gallery_payload(self, media_gallery_item_payload):
+        return {"type": 12, "id": 9267351, "items": [media_gallery_item_payload]}
+
+    @pytest.fixture
+    def separator_payload(self):
+        return {"type": 14, "id": 4920478, "spacing": 1, "divider": True}
+
+    @pytest.fixture
+    def file_payload(self, media_payload):
+        return {"type": 13, "id": 2407385, "file": media_payload, "spoiler": False}
+
+    @pytest.fixture
+    def container_payload(self, file_payload):
+        return {"type": 17, "id": 5830957, "accent_color": 16757027, "spoiler": True, "components": [file_payload]}
+
+    def test__deserialize_media(self, entity_factory_impl, media_payload):
+        media = entity_factory_impl._deserialize_media(media_payload)
+
+        assert media.url == "https://com.com.com.com.com.com.com.com.com.com/"
+        assert (
+            media.proxy_url
+            == "https://example.example.example.example.example.example.example.example.example.example/"
+        )
+        assert media.width == 9845
+        assert media.height == 1904
+        assert media.content_type == "ooga/booga"
+        assert media.loading_state == component_models.MediaLoadingType.LOADED_SUCCESS
+
+        assert isinstance(media, component_models.MediaResource)
+
+    def test__deserialize_media_with_unset_fields(self, entity_factory_impl, media_payload):
+        del media_payload["proxy_url"]
+        del media_payload["width"]
+        del media_payload["height"]
+        del media_payload["content_type"]
+        del media_payload["loading_state"]
+
+        media = entity_factory_impl._deserialize_media(media_payload)
+
+        assert media.proxy_url is None
+        assert media.width is undefined.UNDEFINED
+        assert media.height is undefined.UNDEFINED
+        assert media.content_type is undefined.UNDEFINED
+        assert media.loading_state is undefined.UNDEFINED
+
+        assert isinstance(media, component_models.MediaResource)
+
+    def test__deserialize_media_with_nullable_fields(self, entity_factory_impl, media_payload):
+        media_payload["width"] = None
+        media_payload["height"] = None
+        media_payload["content_type"] = None
+        media_payload["loading_state"] = None
+
+        media = entity_factory_impl._deserialize_media(media_payload)
+
+        assert media.width is None
+        assert media.height is None
+        assert media.content_type is None
+        assert media.loading_state is None
+
+        assert isinstance(media, component_models.MediaResource)
+
+    def test__deserialize_action_row_component(self, entity_factory_impl, action_row_payload, button_payload):
+        action_row = entity_factory_impl._deserialize_action_row_component(action_row_payload)
+
+        assert action_row.type == component_models.ComponentType.ACTION_ROW
+
+        assert action_row.id == 8394572
+        assert action_row.components == [entity_factory_impl._deserialize_button(button_payload)]
+
+        assert isinstance(action_row, component_models.ActionRowComponent)
+
+    def test__deserialize_action_row_component_with_unknown_component_type(
+        self, entity_factory_impl, action_row_payload
+    ):
+        action_row_payload["components"] = [{"type": -9999}, {"type": 9999}]
+
+        action_row = entity_factory_impl._deserialize_action_row_component(action_row_payload)
+
+        assert action_row.components == []
+
+    def test__deserialize_section_component(
+        self, entity_factory_impl, section_payload, button_payload, text_display_payload
+    ):
+        section = entity_factory_impl._deserialize_section_component(section_payload)
+
+        assert isinstance(section, component_models.SectionComponent)
+        assert section.type == component_models.ComponentType.SECTION
+        assert section.id == 9478385
+        assert section.accessory == entity_factory_impl._deserialize_button(button_payload)
+        assert section.components == [entity_factory_impl._deserialize_text_display_component(text_display_payload)]
+
+    def test__deserialize_section_component_with_unknown_accessory_type(self, entity_factory_impl, section_payload):
+        section_payload["accessory"] = {"type": 9999}
+        with pytest.raises(errors.UnrecognisedEntityError, match=r"Unknown section accessory type 9999"):
+            entity_factory_impl._deserialize_section_component(section_payload)
+
+    def test__deserialize_thumbnail_component(self, entity_factory_impl, thumbnail_payload, media_payload):
+        thumbnail = entity_factory_impl._deserialize_thumbnail_component(thumbnail_payload)
+
+        assert thumbnail.type == component_models.ComponentType.THUMBNAIL
+        assert thumbnail.id == 9824133
+        assert thumbnail.media == entity_factory_impl._deserialize_media(media_payload)
+        assert thumbnail.description == "A cool description."
+        assert thumbnail.is_spoiler is False
+
+        assert isinstance(thumbnail, component_models.ThumbnailComponent)
+
+    def test__deserialize_thumbnail_component_with_unset_fields(self, entity_factory_impl, thumbnail_payload):
+        del thumbnail_payload["description"]
+        del thumbnail_payload["spoiler"]
+
+        thumbnail = entity_factory_impl._deserialize_thumbnail_component(thumbnail_payload)
+
+        assert thumbnail.description is None
+        assert thumbnail.is_spoiler is False
+
+    def test__deserialize_text_display_component(self, entity_factory_impl, text_display_payload):
+        text_display = entity_factory_impl._deserialize_text_display_component(text_display_payload)
+
+        assert text_display.type == component_models.ComponentType.TEXT_DISPLAY
+        assert text_display.id == 9840745
+        assert text_display.content == "A text display!"
+
+        assert isinstance(text_display, component_models.TextDisplayComponent)
+
+    def test__deserialize_media_gallery_component(
+        self, entity_factory_impl, media_gallery_payload, media_gallery_item_payload
+    ):
+        media_gallery = entity_factory_impl._deserialize_media_gallery_component(media_gallery_payload)
+
+        assert media_gallery.type == component_models.ComponentType.MEDIA_GALLERY
+        assert media_gallery.id == 9267351
+        assert media_gallery.items == [entity_factory_impl._deserialize_media_gallery_item(media_gallery_item_payload)]
+
+        assert isinstance(media_gallery, component_models.MediaGalleryComponent)
+
+    def test__deserialize_media_gallery_item(self, entity_factory_impl, media_gallery_item_payload, media_payload):
+        media_gallery_item = entity_factory_impl._deserialize_media_gallery_item(media_gallery_item_payload)
+
+        assert media_gallery_item.media == entity_factory_impl._deserialize_media(media_payload)
+        assert media_gallery_item.description == "Gallery item description?"
+        assert media_gallery_item.is_spoiler is True
+
+        assert isinstance(media_gallery_item, component_models.MediaGalleryItem)
+
+    def test__deserialize_media_gallery_item_with_unset_fields(self, entity_factory_impl, media_gallery_item_payload):
+        del media_gallery_item_payload["description"]
+        del media_gallery_item_payload["spoiler"]
+
+        media_gallery_item = entity_factory_impl._deserialize_media_gallery_item(media_gallery_item_payload)
+
+        assert media_gallery_item.description is None
+        assert media_gallery_item.is_spoiler is False
+
+    def test__deserialize_separator_component(self, entity_factory_impl, separator_payload):
+        separator = entity_factory_impl._deserialize_separator_component(separator_payload)
+
+        assert separator.type == component_models.ComponentType.SEPARATOR
+        assert separator.id == 4920478
+        assert separator.spacing == component_models.SpacingType.SMALL
+        assert separator.divider is True
+
+        assert isinstance(separator, component_models.SeparatorComponent)
+
+    def test__deserialize_separator_component_with_unset_fields(self, entity_factory_impl, separator_payload):
+        del separator_payload["divider"]
+
+        separator = entity_factory_impl._deserialize_separator_component(separator_payload)
+
+        assert separator.divider is False
+
+    def test__deserialize_file_component(self, entity_factory_impl, file_payload, media_payload):
+        file = entity_factory_impl._deserialize_file_component(file_payload)
+
+        assert file.type == component_models.ComponentType.FILE
+        assert file.id == 2407385
+        assert file.file == entity_factory_impl._deserialize_media(media_payload)
+        assert file.is_spoiler is False
+
+        assert isinstance(file, component_models.FileComponent)
+
+    def test__deserialize_file_component_with_unset_fields(self, entity_factory_impl, file_payload):
+        del file_payload["spoiler"]
+
+        file = entity_factory_impl._deserialize_file_component(file_payload)
+
+        assert file.is_spoiler is False
+
+    def test__deserialize_container_component(self, entity_factory_impl, container_payload, file_payload):
+        container = entity_factory_impl._deserialize_container_component(container_payload)
+
+        assert container.type == component_models.ComponentType.CONTAINER
+        assert container.id == 5830957
+        assert container.accent_color == color_models.Color.from_hex_code("FFB123")
+        assert container.is_spoiler is True
+        assert container.components == [entity_factory_impl._deserialize_file_component(file_payload)]
+
+        assert isinstance(container, component_models.ContainerComponent)
+
+    def test__deserialize_container_component_with_unset_fields(self, entity_factory_impl, container_payload):
+        del container_payload["accent_color"]
+        del container_payload["spoiler"]
+
+        container = entity_factory_impl._deserialize_container_component(container_payload)
+
+        assert container.accent_color is None
+        assert container.is_spoiler is False
+
+    def test__deserialize_container_component_with_nullable_fields(self, entity_factory_impl, container_payload):
+        container_payload["accent_color"] = None
+
+        container = entity_factory_impl._deserialize_container_component(container_payload)
+
+        assert container.accent_color is None
+
+    def test__deserialize_container_component_with_unknown_component_type(self, entity_factory_impl, container_payload):
+        container_payload["components"] = [{"type": 9999}]
+
+        container = entity_factory_impl._deserialize_container_component(container_payload)
+
+        assert container.components == []
+
+    def test__deserialize_message_components(
+        self,
+        entity_factory_impl,
+        action_row_payload,
+        text_display_payload,
+        section_payload,
+        media_gallery_payload,
+        separator_payload,
+        file_payload,
+    ):
+        message_components = entity_factory_impl._deserialize_top_level_components(
+            [
+                action_row_payload,
+                text_display_payload,
+                section_payload,
+                media_gallery_payload,
+                separator_payload,
+                file_payload,
+            ]
+        )
+
+        assert len(message_components) == 6
+
+        assert message_components[0] == entity_factory_impl._deserialize_action_row_component(action_row_payload)
+
+        assert message_components[1] == entity_factory_impl._deserialize_text_display_component(text_display_payload)
+
+        assert message_components[2] == entity_factory_impl._deserialize_section_component(section_payload)
+
+        assert message_components[3] == entity_factory_impl._deserialize_media_gallery_component(media_gallery_payload)
+
+        assert message_components[4] == entity_factory_impl._deserialize_separator_component(separator_payload)
+
+        assert message_components[5] == entity_factory_impl._deserialize_file_component(file_payload)
+
+    def test__deserialize_message_components_handles_unknown_top_component_type(self, entity_factory_impl):
+        message_components = entity_factory_impl._deserialize_top_level_components([{"type": 9999}, {"type": -9999}])
+
+        assert len(message_components) == 0
+
+    def test__deserialize_modal_components(self, entity_factory_impl, action_row_payload, text_input_payload):
+        action_row_payload["components"] = [text_input_payload]
+
+        modal_components = entity_factory_impl._deserialize_modal_components([action_row_payload])
+
+        assert len(modal_components) == 1
+
+        assert modal_components[0] == component_models.ModalActionRowComponent(
+            type=component_models.ComponentType.ACTION_ROW,
+            id=8394572,
+            components=[entity_factory_impl._deserialize_text_input(text_input_payload)],
+        )
+
+    def test__deserialize_modal_components_handles_unknown_top_component_type(self, entity_factory_impl):
+        modal_components = entity_factory_impl._deserialize_modal_components([{"type": 9999}])
+
+        assert len(modal_components) == 0
+
+    @pytest.mark.skip("Pending removal.")
     @pytest.mark.parametrize(
         ("type_", "fn", "mapping"),
         [
@@ -5749,7 +6091,6 @@ def test__deserialize_text_select_menu_partial(self, entity_factory_impl):
             (6, "_deserialize_select_menu", "_message_component_type_mapping"),
             (7, "_deserialize_select_menu", "_message_component_type_mapping"),
             (8, "_deserialize_channel_select_menu", "_message_component_type_mapping"),
-            (4, "_deserialize_text_input", "_modal_component_type_mapping"),
         ],
     )
     def test__deserialize_components(self, mock_app, type_, fn, mapping):
@@ -5768,6 +6109,7 @@ def test__deserialize_components(self, mock_app, type_, fn, mapping):
         assert isinstance(action_row, component_models.ActionRowComponent)
         assert action_row.components[0] is expected_fn.return_value
 
+    @pytest.mark.skip("Pending removal.")
     def test__deserialize_components_handles_unknown_top_component_type(self, entity_factory_impl):
         components = entity_factory_impl._deserialize_components(
             [
@@ -6141,9 +6483,8 @@ def test_deserialize_partial_message(
         assert partial_message.nonce == "171000788183678976"
         assert partial_message.application_id == 123123123123
 
-        assert partial_message.components == entity_factory_impl._deserialize_components(
-            [action_row_payload], entity_factory_impl._message_component_type_mapping
-        )
+        # Component
+        assert partial_message.components == entity_factory_impl._deserialize_top_level_components([action_row_payload])
 
         # InteractionMetadata
         assert partial_message.interaction_metadata.interaction_id == snowflakes.Snowflake(123456)
@@ -6341,9 +6682,8 @@ def test_deserialize_message(
         assert message.nonce == "171000788183678976"
         assert message.application_id == 123123123123
 
-        assert message.components == entity_factory_impl._deserialize_components(
-            [action_row_payload], entity_factory_impl._message_component_type_mapping
-        )
+        # Component
+        assert message.components == entity_factory_impl._deserialize_top_level_components([action_row_payload])
 
         # Thread
         assert isinstance(message.thread, channel_models.GuildPublicThread)
diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py
index 614170f7..1d65ab4f 100644
--- a/tests/hikari/impl/test_rest.py
+++ b/tests/hikari/impl/test_rest.py
@@ -1260,7 +1260,7 @@ def test__build_message_payload_with_singular_args(self, rest_client):
         attachment = object()
         resource_attachment1 = mock.Mock(filename="attachment.png")
         resource_attachment2 = mock.Mock(filename="attachment2.png")
-        component = mock.Mock(build=mock.Mock(return_value={"component": 1}))
+        component = mock.Mock(build=mock.Mock(return_value=({"component": 1}, ())))
         embed = object()
         embed_attachment = object()
         mentions_everyone = object()
@@ -1337,8 +1337,8 @@ def test__build_message_payload_with_plural_args(self, rest_client):
         resource_attachment4 = mock.Mock(filename="attachment4.png")
         resource_attachment5 = mock.Mock(filename="attachment5.png")
         resource_attachment6 = mock.Mock(filename="attachment6.png")
-        component1 = mock.Mock(build=mock.Mock(return_value={"component": 1}))
-        component2 = mock.Mock(build=mock.Mock(return_value={"component": 2}))
+        component1 = mock.Mock(build=mock.Mock(return_value=({"component": 1}, ())))
+        component2 = mock.Mock(build=mock.Mock(return_value=({"component": 2}, ())))
         embed1 = object()
         embed2 = object()
         embed_attachment1 = object()
@@ -1457,8 +1457,8 @@ def test__build_message_payload_with_edit_and_attachment_object_passed(self, res
         resource_attachment3 = mock.Mock(filename="attachment3.png")
         resource_attachment4 = mock.Mock(filename="attachment4.png")
         resource_attachment5 = mock.Mock(filename="attachment5.png")
-        component1 = mock.Mock(build=mock.Mock(return_value={"component": 1}))
-        component2 = mock.Mock(build=mock.Mock(return_value={"component": 2}))
+        component1 = mock.Mock(build=mock.Mock(return_value=({"component": 1}, ())))
+        component2 = mock.Mock(build=mock.Mock(return_value=({"component": 2}, ())))
         embed1 = object()
         embed2 = object()
         embed_attachment1 = object()
@@ -3219,7 +3219,7 @@ async def test_execute_webhook_when_form(self, rest_client, webhook, avatar_url)
             content_type="application/json",
         )
         rest_client._request.assert_awaited_once_with(
-            expected_route, form_builder=mock_form, query={"wait": "true"}, auth=None
+            expected_route, form_builder=mock_form, query={"wait": "true", "with_components": "true"}, auth=None
         )
         rest_client._entity_factory.deserialize_message.assert_called_once_with({"message_id": 123})
 
@@ -3255,7 +3255,10 @@ async def test_execute_webhook_when_form_and_thread(self, rest_client):
             "payload_json", b'{"testing":"ensure_in_test"}', content_type="application/json"
         )
         rest_client._request.assert_awaited_once_with(
-            expected_route, form_builder=mock_form, query={"wait": "true", "thread_id": "1234543123"}, auth=None
+            expected_route,
+            form_builder=mock_form,
+            query={"wait": "true", "with_components": "true", "thread_id": "1234543123"},
+            auth=None,
         )
         rest_client._entity_factory.deserialize_message.assert_called_once_with({"message_id": 123})
 
@@ -3289,7 +3292,7 @@ async def test_execute_webhook_when_no_form(self, rest_client):
         rest_client._request.assert_awaited_once_with(
             expected_route,
             json={"testing": "ensure_in_test"},
-            query={"wait": "true", "thread_id": "2134312123"},
+            query={"wait": "true", "with_components": "true", "thread_id": "2134312123"},
             auth=None,
         )
         rest_client._entity_factory.deserialize_message.assert_called_once_with({"message_id": 123})
@@ -3347,7 +3350,7 @@ async def test_execute_webhook_when_thread_and_no_form(self, rest_client):
         rest_client._request.assert_awaited_once_with(
             expected_route,
             json={"testing": "ensure_in_test", "username": "davfsa", "avatar_url": "https://website.com/davfsa_logo"},
-            query={"wait": "true"},
+            query={"wait": "true", "with_components": "true"},
             auth=None,
         )
         rest_client._entity_factory.deserialize_message.assert_called_once_with({"message_id": 123})
@@ -3426,7 +3429,9 @@ async def test_edit_webhook_message_when_form(self, rest_client, webhook):
         mock_form.add_field.assert_called_once_with(
             "payload_json", b'{"testing":"ensure_in_test"}', content_type="application/json"
         )
-        rest_client._request.assert_awaited_once_with(expected_route, form_builder=mock_form, query={}, auth=None)
+        rest_client._request.assert_awaited_once_with(
+            expected_route, form_builder=mock_form, query={"with_components": "true"}, auth=None
+        )
         rest_client._entity_factory.deserialize_message.assert_called_once_with({"message_id": 123})
 
     async def test_edit_webhook_message_when_form_and_thread(self, rest_client):
@@ -3459,7 +3464,10 @@ async def test_edit_webhook_message_when_form_and_thread(self, rest_client):
             "payload_json", b'{"testing":"ensure_in_test"}', content_type="application/json"
         )
         rest_client._request.assert_awaited_once_with(
-            expected_route, form_builder=mock_form, query={"thread_id": "123543123"}, auth=None
+            expected_route,
+            form_builder=mock_form,
+            query={"with_components": "true", "thread_id": "123543123"},
+            auth=None,
         )
         rest_client._entity_factory.deserialize_message.assert_called_once_with({"message_id": 123})
 
@@ -3507,7 +3515,7 @@ async def test_edit_webhook_message_when_no_form(self, rest_client: rest_api.RES
             edit=True,
         )
         rest_client._request.assert_awaited_once_with(
-            expected_route, json={"testing": "ensure_in_test"}, query={}, auth=None
+            expected_route, json={"testing": "ensure_in_test"}, query={"with_components": "true"}, auth=None
         )
         rest_client._entity_factory.deserialize_message.assert_called_once_with({"message_id": 123})
 
@@ -3537,7 +3545,10 @@ async def test_edit_webhook_message_when_thread_and_no_form(self, rest_client: r
             edit=True,
         )
         rest_client._request.assert_awaited_once_with(
-            expected_route, json={"testing": "ensure_in_test"}, query={"thread_id": "2346523432"}, auth=None
+            expected_route,
+            json={"testing": "ensure_in_test"},
+            query={"with_components": "true", "thread_id": "2346523432"},
+            auth=None,
         )
         rest_client._entity_factory.deserialize_message.assert_called_once_with({"message_id": 123})
 
diff --git a/tests/hikari/impl/test_special_endpoints.py b/tests/hikari/impl/test_special_endpoints.py
index 37de3d48..89ceba48 100644
--- a/tests/hikari/impl/test_special_endpoints.py
+++ b/tests/hikari/impl/test_special_endpoints.py
@@ -27,6 +27,7 @@
 
 from hikari import applications
 from hikari import channels
+from hikari import colors
 from hikari import commands
 from hikari import components
 from hikari import emojis
@@ -1314,6 +1315,7 @@ def test_set_is_disabled(self, button):
 
     def test_build(self):
         button = special_endpoints._ButtonBuilder(
+            id=5855932,
             style=components.ButtonStyle.DANGER,
             url="https://example.com",
             label="no u",
@@ -1322,7 +1324,10 @@ def test_build(self):
             is_disabled=True,
         )
 
-        assert button.build() == {
+        payload, attachments = button.build()
+
+        assert payload == {
+            "id": 5855932,
             "type": components.ComponentType.BUTTON,
             "style": components.ButtonStyle.DANGER,
             "url": "https://example.com",
@@ -1332,30 +1337,40 @@ def test_build(self):
             "disabled": True,
         }
 
+        assert attachments == []
+
     @pytest.mark.parametrize("emoji", [123321, emojis.CustomEmoji(id=123321, name="", is_animated=True)])
     def test_build_with_custom_emoji(self, emoji: typing.Union[int, emojis.Emoji]):
         button = special_endpoints._ButtonBuilder(
             style=components.ButtonStyle.DANGER, emoji=emoji, url=undefined.UNDEFINED, custom_id=undefined.UNDEFINED
         )
 
-        assert button.build() == {
+        payload, attachments = button.build()
+
+        assert payload == {
             "type": components.ComponentType.BUTTON,
             "style": components.ButtonStyle.DANGER,
             "emoji": {"id": "123321"},
             "disabled": False,
         }
 
+        assert attachments == []
+
     def test_build_without_optional_fields(self):
         button = special_endpoints._ButtonBuilder(
             style=components.ButtonStyle.LINK, url=undefined.UNDEFINED, custom_id=undefined.UNDEFINED
         )
 
-        assert button.build() == {
+        payload, attachments = button.build()
+
+        assert payload == {
             "type": components.ComponentType.BUTTON,
             "style": components.ButtonStyle.LINK,
             "disabled": False,
         }
 
+        assert attachments == []
+
 
 class TestLinkButtonBuilder:
     def test_url_property(self):
@@ -1491,6 +1506,7 @@ def test_set_max_values(self, menu):
 
     def test_build(self):
         menu = special_endpoints.SelectMenuBuilder(
+            id=5855932,
             custom_id="45234fsdf",
             type=components.ComponentType.USER_SELECT_MENU,
             placeholder="meep",
@@ -1499,7 +1515,10 @@ def test_build(self):
             is_disabled=True,
         )
 
-        assert menu.build() == {
+        payload, attachments = menu.build()
+
+        assert payload == {
+            "id": 5855932,
             "type": components.ComponentType.USER_SELECT_MENU,
             "custom_id": "45234fsdf",
             "placeholder": "meep",
@@ -1508,10 +1527,14 @@ def test_build(self):
             "max_values": 23,
         }
 
+        assert attachments == []
+
     def test_build_without_optional_fields(self):
         menu = special_endpoints.SelectMenuBuilder(custom_id="o2o2o2", type=components.ComponentType.ROLE_SELECT_MENU)
 
-        assert menu.build() == {
+        payload, attachments = menu.build()
+
+        assert payload == {
             "type": components.ComponentType.ROLE_SELECT_MENU,
             "custom_id": "o2o2o2",
             "disabled": False,
@@ -1519,6 +1542,8 @@ def test_build_without_optional_fields(self):
             "max_values": 1,
         }
 
+        assert attachments == []
+
 
 class TestTextSelectMenuBuilder:
     @pytest.fixture
@@ -1560,6 +1585,7 @@ def test_add_raw_option(self, menu):
 
     def test_build(self):
         menu = special_endpoints.TextSelectMenuBuilder(
+            id=5855932,
             custom_id="o2o2o2",
             placeholder="hi",
             min_values=22,
@@ -1568,7 +1594,10 @@ def test_build(self):
             options=[special_endpoints.SelectOptionBuilder("meow", "vault")],
         )
 
-        assert menu.build() == {
+        payload, attachments = menu.build()
+
+        assert payload == {
+            "id": 5855932,
             "type": components.ComponentType.TEXT_SELECT_MENU,
             "custom_id": "o2o2o2",
             "placeholder": "hi",
@@ -1578,10 +1607,14 @@ def test_build(self):
             "options": [{"label": "meow", "value": "vault", "default": False}],
         }
 
+        assert attachments == []
+
     def test_build_without_optional_fields(self):
         menu = special_endpoints.TextSelectMenuBuilder(custom_id="fds  qw")
 
-        assert menu.build() == {
+        payload, attachments = menu.build()
+
+        assert payload == {
             "type": components.ComponentType.TEXT_SELECT_MENU,
             "custom_id": "fds  qw",
             "min_values": 0,
@@ -1590,6 +1623,8 @@ def test_build_without_optional_fields(self):
             "options": [],
         }
 
+        assert attachments == []
+
 
 class TestChannelSelectMenuBuilder:
     def test_type_property(self):
@@ -1605,6 +1640,7 @@ def test_set_channel_types(self):
 
     def test_build(self):
         menu = special_endpoints.ChannelSelectMenuBuilder(
+            id=5855932,
             custom_id="o2o2o2",
             placeholder="hi",
             min_values=22,
@@ -1613,7 +1649,10 @@ def test_build(self):
             channel_types=[channels.ChannelType.GUILD_CATEGORY],
         )
 
-        assert menu.build() == {
+        payload, attachments = menu.build()
+
+        assert payload == {
+            "id": 5855932,
             "type": components.ComponentType.CHANNEL_SELECT_MENU,
             "custom_id": "o2o2o2",
             "placeholder": "hi",
@@ -1623,10 +1662,14 @@ def test_build(self):
             "channel_types": [channels.ChannelType.GUILD_CATEGORY],
         }
 
+        assert attachments == []
+
     def test_build_without_optional_fields(self):
         menu = special_endpoints.ChannelSelectMenuBuilder(custom_id="42312312")
 
-        assert menu.build() == {
+        payload, attachments = menu.build()
+
+        assert payload == {
             "type": components.ComponentType.CHANNEL_SELECT_MENU,
             "custom_id": "42312312",
             "min_values": 0,
@@ -1635,6 +1678,8 @@ def test_build_without_optional_fields(self):
             "channel_types": [],
         }
 
+        assert attachments == []
+
 
 class TestTextInput:
     @pytest.fixture
@@ -1679,7 +1724,9 @@ def test_set_max_length(self, text_input):
     def test_build_partial(self):
         text_input = special_endpoints.TextInputBuilder(custom_id="o2o2o2", label="label")
 
-        assert text_input.build() == {
+        payload, attachments = text_input.build()
+
+        assert payload == {
             "type": components.ComponentType.TEXT_INPUT,
             "style": 1,
             "custom_id": "o2o2o2",
@@ -1689,8 +1736,11 @@ def test_build_partial(self):
             "max_length": 4000,
         }
 
+        assert attachments == []
+
     def test_build(self):
         text_input = special_endpoints.TextInputBuilder(
+            id=5855932,
             custom_id="o2o2o2",
             label="label",
             placeholder="placeholder",
@@ -1700,7 +1750,10 @@ def test_build(self):
             max_length=250,
         )
 
-        assert text_input.build() == {
+        payload, attachments = text_input.build()
+
+        assert payload == {
+            "id": 5855932,
             "type": components.ComponentType.TEXT_INPUT,
             "style": 1,
             "custom_id": "o2o2o2",
@@ -1712,6 +1765,8 @@ def test_build(self):
             "max_length": 250,
         }
 
+        assert attachments == []
+
 
 class TestMessageActionRowBuilder:
     def test_type_property(self):
@@ -1805,20 +1860,500 @@ def test_add_text_menu(self):
         assert component.is_disabled is True
 
     def test_build(self):
-        mock_component_1 = mock.Mock(type=components.ComponentType.BUTTON)
-        mock_component_2 = mock.Mock(type=components.ComponentType.BUTTON)
-
-        row = (
-            special_endpoints.MessageActionRowBuilder().add_component(mock_component_1).add_component(mock_component_2)
+        mock_component_1 = mock.Mock(
+            special_endpoints.InteractiveButtonBuilder,
+            build=mock.Mock(return_value=(mock.Mock(type=components.ComponentType.BUTTON), ())),
+        )
+        mock_component_2 = mock.Mock(
+            special_endpoints.InteractiveButtonBuilder,
+            build=mock.Mock(return_value=(mock.Mock(type=components.ComponentType.BUTTON), ())),
         )
 
-        assert row.build() == {
+        row = special_endpoints.MessageActionRowBuilder(id=5855932, components=[mock_component_1, mock_component_2])
+
+        payload, attachments = row.build()
+
+        assert payload == {
             "type": components.ComponentType.ACTION_ROW,
-            "components": [mock_component_1.build.return_value, mock_component_2.build.return_value],
+            "id": 5855932,
+            "components": [mock_component_1.build.return_value[0], mock_component_2.build.return_value[0]],
         }
         mock_component_1.build.assert_called_once_with()
         mock_component_2.build.assert_called_once_with()
 
+        assert attachments == []
+
+
+class TestSectionComponentBuilder:
+    def test_type_property(self):
+        section = special_endpoints.SectionComponentBuilder(accessory=mock.Mock())
+
+        assert section.type is components.ComponentType.SECTION
+
+    def test_add_component(self):
+        section = special_endpoints.SectionComponentBuilder(accessory=mock.Mock())
+
+        assert section.components == []
+
+        text_display = special_endpoints.TextDisplayComponentBuilder(content="test content")
+
+        section.add_component(text_display)
+
+        assert section.components == [text_display]
+
+    def test_add_text_display(self):
+        section = special_endpoints.SectionComponentBuilder(accessory=mock.Mock())
+
+        assert section.components == []
+
+        section.add_text_display("test content")
+
+        assert len(section.components) == 1
+
+        assert section.components[0].content == "test content"
+
+    def test_build(self):
+        accessory = special_endpoints.ThumbnailComponentBuilder(
+            id=2193485, media="some-test-file.png", description="a cool image", spoiler=False
+        )
+
+        section = special_endpoints.SectionComponentBuilder(id=5855932, accessory=accessory)
+        section.add_text_display("A display?", id=4893723)
+        section.add_text_display("Yes, a display.", id=9018345)
+
+        payload, attachments = section.build()
+
+        assert payload == {
+            "type": components.ComponentType.SECTION,
+            "id": 5855932,
+            "accessory": {
+                "type": components.ComponentType.THUMBNAIL,
+                "id": 2193485,
+                "media": {"url": "attachment://some-test-file.png"},
+                "description": "a cool image",
+                "spoiler": False,
+            },
+            "components": [
+                {"type": components.ComponentType.TEXT_DISPLAY, "id": 4893723, "content": "A display?"},
+                {"type": components.ComponentType.TEXT_DISPLAY, "id": 9018345, "content": "Yes, a display."},
+            ],
+        }
+        assert attachments == [files.ensure_resource("some-test-file.png")]
+
+    def test_build_without_optional_fields(self):
+        section = special_endpoints.SectionComponentBuilder(
+            accessory=special_endpoints.ThumbnailComponentBuilder(
+                media="some-test-file.png", description="a cool image", spoiler=False
+            )
+        )
+
+        payload, attachments = section.build()
+
+        assert payload == {
+            "type": components.ComponentType.SECTION,
+            "accessory": {
+                "type": components.ComponentType.THUMBNAIL,
+                "media": {"url": "attachment://some-test-file.png"},
+                "description": "a cool image",
+                "spoiler": False,
+            },
+            "components": [],
+        }
+        assert attachments == [files.ensure_resource("some-test-file.png")]
+
+
+class TestTextDisplayComponentBuilder:
+    def test_type_property(self):
+        text_display = special_endpoints.TextDisplayComponentBuilder(content="A display?")
+
+        assert text_display.type is components.ComponentType.TEXT_DISPLAY
+
+    def test_build(self):
+        text_display = special_endpoints.TextDisplayComponentBuilder(id=5855932, content="A display?")
+
+        payload, attachments = text_display.build()
+
+        assert payload == {"type": components.ComponentType.TEXT_DISPLAY, "id": 5855932, "content": "A display?"}
+
+        assert attachments == ()
+
+
+class TestThumbnailComponentBuilder:
+    def test_type_property(self):
+        thumbnail = special_endpoints.ThumbnailComponentBuilder(media=mock.Mock())
+
+        assert thumbnail.type is components.ComponentType.THUMBNAIL
+
+    def test_build(self):
+        thumbnail = special_endpoints.ThumbnailComponentBuilder(
+            id=5855932, media="some-test-file.png", description="a cool image", spoiler=False
+        )
+
+        payload, attachments = thumbnail.build()
+
+        assert payload == {
+            "type": components.ComponentType.THUMBNAIL,
+            "id": 5855932,
+            "media": {"url": "attachment://some-test-file.png"},
+            "description": "a cool image",
+            "spoiler": False,
+        }
+
+        assert attachments == (files.ensure_resource("some-test-file.png"),)
+
+    def test_build_without_optional_fields(self):
+        thumbnail = special_endpoints.ThumbnailComponentBuilder(media="some-test-file.png")
+
+        payload, attachments = thumbnail.build()
+
+        assert payload == {
+            "type": components.ComponentType.THUMBNAIL,
+            "media": {"url": "attachment://some-test-file.png"},
+            "spoiler": False,
+        }
+
+        assert attachments == (files.ensure_resource("some-test-file.png"),)
+
+
+class TestMediaGalleryComponentBuilder:
+    def test_type_property(self):
+        media_gallery = special_endpoints.MediaGalleryComponentBuilder()
+
+        assert media_gallery.type is components.ComponentType.MEDIA_GALLERY
+
+    def test_add_item(self):
+        media_gallery = special_endpoints.MediaGalleryComponentBuilder()
+
+        assert media_gallery.items == []
+
+        media_gallery_item = special_endpoints.MediaGalleryItemBuilder(
+            media="some-test-file.png", description="Some description", spoiler=False
+        )
+
+        media_gallery.add_item(media_gallery_item)
+
+        assert media_gallery.items == [media_gallery_item]
+
+    def test_add_media_gallery_item(self):
+        media_gallery = special_endpoints.MediaGalleryComponentBuilder()
+
+        assert media_gallery.items == []
+
+        media_gallery.add_media_gallery_item("some-test-file.png", description="Some description", spoiler=False)
+
+        assert len(media_gallery.items) == 1
+
+        assert media_gallery.items[0].media == "some-test-file.png"
+        assert media_gallery.items[0].description == "Some description"
+        assert media_gallery.items[0].is_spoiler is False
+
+    def test_build(self):
+        media_gallery = special_endpoints.MediaGalleryComponentBuilder(id=5855932)
+
+        media_gallery.add_media_gallery_item("some-test-file.png", description="Some description", spoiler=False)
+
+        media_gallery.add_media_gallery_item("some-test-file2.png", description="Some description 2", spoiler=True)
+
+        payload, attachments = media_gallery.build()
+
+        assert payload == {
+            "type": components.ComponentType.MEDIA_GALLERY,
+            "id": 5855932,
+            "items": [
+                {
+                    "media": {"url": "attachment://some-test-file.png"},
+                    "description": "Some description",
+                    "spoiler": False,
+                },
+                {
+                    "media": {"url": "attachment://some-test-file2.png"},
+                    "description": "Some description 2",
+                    "spoiler": True,
+                },
+            ],
+        }
+
+        assert attachments == [
+            files.ensure_resource("some-test-file.png"),
+            files.ensure_resource("some-test-file2.png"),
+        ]
+
+    def test_build_without_optional_fields(self):
+        media_gallery = special_endpoints.MediaGalleryComponentBuilder()
+
+        payload, attachments = media_gallery.build()
+
+        assert payload == {"type": components.ComponentType.MEDIA_GALLERY, "items": []}
+
+        assert attachments == []
+
+
+class TestMediaGalleryItemBuilder:
+    def test_build(self):
+        media_gallery_item = special_endpoints.MediaGalleryItemBuilder(
+            media="some-test-file.png", description="Some description", spoiler=False
+        )
+
+        payload, attachments = media_gallery_item.build()
+
+        assert payload == {
+            "media": {"url": "attachment://some-test-file.png"},
+            "description": "Some description",
+            "spoiler": False,
+        }
+        assert attachments == (files.ensure_resource("attachment://some-test-file.png"),)
+
+    def test_build_without_optional_fields(self):
+        media_gallery_item = special_endpoints.MediaGalleryItemBuilder(media="some-test-file.png")
+
+        payload, attachments = media_gallery_item.build()
+
+        assert payload == {"media": {"url": "attachment://some-test-file.png"}, "spoiler": False}
+        assert attachments == (files.ensure_resource("some-test-file.png"),)
+
+
+class TestSeparatorComponentBuilder:
+    def test_type_property(self):
+        separator = special_endpoints.SeparatorComponentBuilder()
+
+        assert separator.type is components.ComponentType.SEPARATOR
+
+    def test_build(self):
+        separator = special_endpoints.SeparatorComponentBuilder(
+            id=5855932, spacing=components.SpacingType.SMALL, divider=True
+        )
+
+        payload, attachments = separator.build()
+
+        assert payload == {
+            "type": components.ComponentType.SEPARATOR,
+            "id": 5855932,
+            "spacing": components.SpacingType.SMALL,
+            "divider": True,
+        }
+        assert attachments == ()
+
+    def test_build_without_optional_fields(self):
+        separator = special_endpoints.SeparatorComponentBuilder()
+
+        payload, attachments = separator.build()
+
+        assert payload == {"type": components.ComponentType.SEPARATOR}
+        assert attachments == ()
+
+
+class TestFileComponentBuilder:
+    def test_type_property(self):
+        file = special_endpoints.FileComponentBuilder(file=mock.Mock())
+
+        assert file.type is components.ComponentType.FILE
+
+    def test_build(self):
+        file = special_endpoints.FileComponentBuilder(
+            id=5855932, file="https://example.com/some-test-file.png", spoiler=True
+        )
+
+        payload, attachments = file.build()
+
+        assert payload == {
+            "type": components.ComponentType.FILE,
+            "id": 5855932,
+            "file": {"url": "https://example.com/some-test-file.png"},
+            "spoiler": True,
+        }
+        assert attachments == (files.ensure_resource("https://example.com/some-test-file.png"),)
+
+    def test_build_without_optional_fields(self):
+        file = special_endpoints.FileComponentBuilder(file="some-test-file.png")
+
+        payload, attachments = file.build()
+
+        assert payload == {
+            "type": components.ComponentType.FILE,
+            "file": {"url": "attachment://some-test-file.png"},
+            "spoiler": False,
+        }
+        assert attachments == (files.ensure_resource("some-test-file.png"),)
+
+
+class TestMessageContainerBuilder:
+    def test_type_property(self):
+        container = special_endpoints.ContainerComponentBuilder()
+
+        assert container.type is components.ComponentType.CONTAINER
+
+    def test_add_component(self):
+        container = special_endpoints.ContainerComponentBuilder()
+
+        assert container.components == []
+
+        component = special_endpoints.SeparatorComponentBuilder()
+
+        container.add_component(component)
+
+        assert container.components == [component]
+
+    def test_add_action_row(self):
+        container = special_endpoints.ContainerComponentBuilder()
+
+        assert container.components == []
+
+        button = special_endpoints.InteractiveButtonBuilder(
+            style=components.ButtonStyle.DANGER, custom_id="button", label="test button"
+        )
+
+        container.add_action_row([button])
+
+        assert len(container.components) == 1
+
+        component = container.components[0]
+
+        assert isinstance(component, special_endpoints.MessageActionRowBuilder)
+
+        assert component.components == [button]
+
+    def test_add_text_display(self):
+        container = special_endpoints.ContainerComponentBuilder()
+
+        assert container.components == []
+
+        container.add_text_display("A text display!")
+
+        assert len(container.components) == 1
+
+        component = container.components[0]
+
+        assert isinstance(component, special_endpoints.TextDisplayComponentBuilder)
+
+        assert component.content == "A text display!"
+
+    def test_add_media_gallery(self):
+        container = special_endpoints.ContainerComponentBuilder()
+
+        assert container.components == []
+
+        media_gallery_item = special_endpoints.MediaGalleryItemBuilder(
+            media="some-test-file.png", description="Some description", spoiler=False
+        )
+        container.add_media_gallery([media_gallery_item])
+
+        assert len(container.components) == 1
+        component = container.components[0]
+        assert isinstance(component, special_endpoints.MediaGalleryComponentBuilder)
+        assert component.items == [media_gallery_item]
+
+    def test_add_separator(self):
+        container = special_endpoints.ContainerComponentBuilder()
+
+        assert container.components == []
+
+        container.add_separator(spacing=components.SpacingType.LARGE, divider=False)
+
+        assert len(container.components) == 1
+
+        component = container.components[0]
+
+        assert isinstance(component, special_endpoints.SeparatorComponentBuilder)
+
+        assert component.spacing == components.SpacingType.LARGE
+        assert component.divider is False
+
+    def test_add_file(self):
+        container = special_endpoints.ContainerComponentBuilder()
+
+        assert container.components == []
+
+        container.add_file(file="some-test-file.png", spoiler=True)
+
+        assert len(container.components) == 1
+        component = container.components[0]
+        assert isinstance(component, special_endpoints.FileComponentBuilder)
+        assert component.file == "some-test-file.png"
+        assert component.is_spoiler is True
+
+    def test_build(self):
+        accent_color = colors.Color.from_hex_code("#FFB123")
+        mock_button = mock.Mock(
+            special_endpoints.InteractiveButtonBuilder,
+            build=mock.Mock(return_value=(mock.Mock(type=components.ComponentType.BUTTON), ())),
+        )
+        media_gallery_item = special_endpoints.MediaGalleryItemBuilder(
+            media="some-test-file.png", description="Some description", spoiler=False
+        )
+
+        container = special_endpoints.ContainerComponentBuilder(id=5855932, accent_color=accent_color, spoiler=True)
+        container.add_action_row([mock_button], id=3204958)
+        container.add_text_display("A text display!", id=8944352)
+        container.add_media_gallery([media_gallery_item], id=1098573)
+        container.add_separator(spacing=components.SpacingType.LARGE, divider=False, id=9542323)
+        container.add_file(file="file.txt", spoiler=True, id=2339534)
+
+        payload, attachments = container.build()
+
+        assert payload == {
+            "type": components.ComponentType.CONTAINER,
+            "id": 5855932,
+            "accent_color": accent_color,
+            "spoiler": True,
+            "components": [
+                {
+                    "type": components.ComponentType.ACTION_ROW,
+                    "id": 3204958,
+                    "components": [mock_button.build.return_value[0]],
+                },
+                {"type": components.ComponentType.TEXT_DISPLAY, "id": 8944352, "content": "A text display!"},
+                {
+                    "type": components.ComponentType.MEDIA_GALLERY,
+                    "id": 1098573,
+                    "items": [
+                        {
+                            "media": {"url": "attachment://some-test-file.png"},
+                            "description": "Some description",
+                            "spoiler": False,
+                        }
+                    ],
+                },
+                {
+                    "type": components.ComponentType.SEPARATOR,
+                    "id": 9542323,
+                    "spacing": components.SpacingType.LARGE,
+                    "divider": False,
+                },
+                {
+                    "type": components.ComponentType.FILE,
+                    "id": 2339534,
+                    "file": {"url": "attachment://file.txt"},
+                    "spoiler": True,
+                },
+            ],
+        }
+
+        mock_button.build.assert_called_once_with()
+
+        assert attachments == [files.ensure_resource("some-test-file.png"), files.ensure_resource("file.txt")]
+
+    def test_build_without_optional_fields(self):
+        container = special_endpoints.ContainerComponentBuilder(accent_color=None)
+
+        payload, attachments = container.build()
+
+        assert payload == {
+            "type": components.ComponentType.CONTAINER,
+            "accent_color": None,
+            "spoiler": False,
+            "components": [],
+        }
+
+        assert attachments == []
+
+    def test_build_without_undefined_fields(self):
+        container = special_endpoints.ContainerComponentBuilder()
+
+        payload, attachments = container.build()
+
+        assert payload == {"type": components.ComponentType.CONTAINER, "spoiler": False, "components": []}
+        assert attachments == []
+
 
 class TestModalActionRow:
     def test_type_property(self):
@@ -1852,20 +2387,30 @@ def test_add_text_input(self):
         assert menu.max_length == 447
 
     def test_build(self):
-        mock_component_1 = mock.Mock(type=components.ComponentType.TEXT_INPUT)
-        mock_component_2 = mock.Mock(type=components.ComponentType.TEXT_INPUT)
+        mock_component_1 = mock.Mock(
+            special_endpoints.InteractiveButtonBuilder,
+            build=mock.Mock(return_value=(mock.Mock(type=components.ComponentType.TEXT_INPUT), ())),
+        )
+        mock_component_2 = mock.Mock(
+            special_endpoints.InteractiveButtonBuilder,
+            build=mock.Mock(return_value=(mock.Mock(type=components.ComponentType.TEXT_INPUT), ())),
+        )
 
-        row = special_endpoints.ModalActionRowBuilder().add_component(mock_component_1).add_component(mock_component_2)
+        row = special_endpoints.ModalActionRowBuilder(id=5855932, components=[mock_component_1, mock_component_2])
 
-        result = row.build()
+        payload, attachments = row.build()
 
-        assert result == {
+        assert payload == {
             "type": components.ComponentType.ACTION_ROW,
-            "components": [mock_component_1.build.return_value, mock_component_2.build.return_value],
+            "id": 5855932,
+            "components": [mock_component_1.build.return_value[0], mock_component_2.build.return_value[0]],
         }
+
         mock_component_1.build.assert_called_once_with()
         mock_component_2.build.assert_called_once_with()
 
+        assert attachments == []
+
 
 class TestPollBuilder:
     def test_add_answer(self):
diff --git a/tests/hikari/interactions/test_modal_interactions.py b/tests/hikari/interactions/test_modal_interactions.py
index b7f6f7f4..a9deebac 100644
--- a/tests/hikari/interactions/test_modal_interactions.py
+++ b/tests/hikari/interactions/test_modal_interactions.py
@@ -58,11 +58,12 @@ def mock_modal_interaction(self, mock_app):
             guild_locale="en-US",
             app_permissions=543123,
             components=special_endpoints.ModalActionRowBuilder(
+                id=9817398,
                 components=[
                     components.TextInputComponent(
-                        type=components.ComponentType.TEXT_INPUT, custom_id="le id", value="le value"
+                        type=components.ComponentType.TEXT_INPUT, id=9817398, custom_id="le id", value="le value"
                     )
-                ]
+                ],
             ),
             entitlements=[
                 monetization.Entitlement(
diff --git a/tests/hikari/test_components.py b/tests/hikari/test_components.py
index ccbd8fa3..7365645b 100644
--- a/tests/hikari/test_components.py
+++ b/tests/hikari/test_components.py
@@ -26,25 +26,27 @@
 class TestActionRowComponent:
     def test_getitem_operator_with_index(self):
         mock_component = object()
-        row = components.ActionRowComponent(type=1, components=[object(), mock_component, object()])
+        row = components.ActionRowComponent(type=1, id=5855932, components=[object(), mock_component, object()])
 
         assert row[1] is mock_component
 
     def test_getitem_operator_with_slice(self):
         mock_component_1 = object()
         mock_component_2 = object()
-        row = components.ActionRowComponent(type=1, components=[object(), mock_component_1, object(), mock_component_2])
+        row = components.ActionRowComponent(
+            type=1, id=5855932, components=[object(), mock_component_1, object(), mock_component_2]
+        )
 
         assert row[1:4:2] == [mock_component_1, mock_component_2]
 
     def test_iter_operator(self):
         mock_component_1 = object()
         mock_component_2 = object()
-        row = components.ActionRowComponent(type=1, components=[mock_component_1, mock_component_2])
+        row = components.ActionRowComponent(type=1, id=5855932, components=[mock_component_1, mock_component_2])
 
         assert list(row) == [mock_component_1, mock_component_2]
 
     def test_len_operator(self):
-        row = components.ActionRowComponent(type=1, components=[object(), object()])
+        row = components.ActionRowComponent(type=1, id=5855932, components=[object(), object()])
 
         assert len(row) == 2