diff --git a/vizro-core/changelog.d/20231030_132538_maximilian_schulz_table_component_MS.md b/vizro-core/changelog.d/20231030_132538_maximilian_schulz_table_component_MS.md
new file mode 100644
index 000000000..bbebc9f4c
--- /dev/null
+++ b/vizro-core/changelog.d/20231030_132538_maximilian_schulz_table_component_MS.md
@@ -0,0 +1,44 @@
+<!--
+A new scriv changelog fragment.
+
+Uncomment the section that is right (remove the HTML comment wrapper).
+-->
+
+### Highlights ✨
+
+- Release of the Vizro Dash DataTable. Visit the [user guide on tables](https://vizro.readthedocs.io/en/stable/pages/user_guides/table/) to learn more. ([#114](https://github.com/mckinsey/vizro/pull/114))
+
+<!--
+### Removed
+
+- A bullet item for the Removed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
+
+-->
+<!-- ### Added
+
+- A bullet item for the Added category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#114](https://github.com/mckinsey/vizro/pull/114)) -->
+
+<!--
+### Changed
+
+- A bullet item for the Changed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
+
+-->
+<!--
+### Deprecated
+
+- A bullet item for the Deprecated category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
+
+-->
+<!--
+### Fixed
+
+- A bullet item for the Fixed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
+
+-->
+<!--
+### Security
+
+- A bullet item for the Security category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
+
+-->
diff --git a/vizro-core/docs/assets/user_guides/table/custom_table.png b/vizro-core/docs/assets/user_guides/table/custom_table.png
new file mode 100644
index 000000000..990d57a85
Binary files /dev/null and b/vizro-core/docs/assets/user_guides/table/custom_table.png differ
diff --git a/vizro-core/docs/assets/user_guides/table/styled_table.png b/vizro-core/docs/assets/user_guides/table/styled_table.png
new file mode 100644
index 000000000..773aaa4b3
Binary files /dev/null and b/vizro-core/docs/assets/user_guides/table/styled_table.png differ
diff --git a/vizro-core/docs/assets/user_guides/table/table.png b/vizro-core/docs/assets/user_guides/table/table.png
new file mode 100644
index 000000000..d9f960dd1
Binary files /dev/null and b/vizro-core/docs/assets/user_guides/table/table.png differ
diff --git a/vizro-core/docs/pages/user_guides/components.md b/vizro-core/docs/pages/user_guides/components.md
index 1ca750935..0018964bd 100755
--- a/vizro-core/docs/pages/user_guides/components.md
+++ b/vizro-core/docs/pages/user_guides/components.md
@@ -1,89 +1,9 @@
-# How to use charts/components
+# How to use cards & buttons
 
-This guide shows you how to use charts/components to visualize your data in the dashboard.
+This guide shows you how to use cards and buttons to visualize and interact with your data in the dashboard.
 
 The [`Page`][vizro.models.Page] models accepts the `components` argument, where you can enter your visual content e.g.
-[`Graph`][vizro.models.Graph], [`Card`][vizro.models.Card] or [`Button`][vizro.models.Button].
-
-## Graph
-
-The [`Graph`][vizro.models.Graph] model is the most used component in many dashboards, allowing you to visualize data in a variety of ways.
-
-You can add a [`Graph`][vizro.models.Graph]
-to your dashboard by inserting the [`Graph`][vizro.models.Graph] model into the `components` argument of the
-[`Page`][vizro.models.Page] model. You will need to specify the `figure` argument, where you can enter any of the
-currently available charts of the open source library [`plotly.express`](https://plotly.com/python/plotly-express/).
-
-!!! note
-
-    Note that in order to use the [`plotly.express`](https://plotly.com/python/plotly-express/) chart in a Vizro dashboard, you need to import it as `import vizro.plotly.express as px`.
-    This leaves any of the [`plotly.express`](https://plotly.com/python/plotly-express/) functionality untouched, but allows _direct insertion_ into the [`Graph`][vizro.models.Graph] model _as is_.
-
-!!! example "Graph"
-    === "app.py"
-        ```py
-        import vizro.models as vm
-        import vizro.plotly.express as px
-        from vizro import Vizro
-
-        df = px.data.iris()
-
-        page = vm.Page(
-            title="My first page",
-            components=[
-                vm.Graph(
-                    id="my_chart",
-                    figure=px.scatter_matrix(
-                        df, dimensions=["sepal_length", "sepal_width", "petal_length", "petal_width"], color="species"
-                    ),
-                ),
-            ],
-            controls=[vm.Filter(column="species", selector=vm.Dropdown(title="Species"))],
-        )
-
-        dashboard = vm.Dashboard(pages=[page])
-
-        Vizro().build(dashboard).run()
-        ```
-    === "app.yaml"
-        ```yaml
-        # Still requires a .py to register data connector in Data Manager and parse yaml configuration
-        # See from_yaml example
-        pages:
-        - components:
-          - figure:
-              _target_: scatter_matrix
-              color: species
-              data_frame: iris
-              dimensions: ["sepal_length", "sepal_width", "petal_length", "petal_width"]
-            id: my_chart
-            type: graph
-          controls:
-            - column: continent
-              type: filter
-            - selector:
-                title: Species
-                type: dropdown
-          title: My first page
-        ```
-    === "Result"
-        [![Graph]][Graph]
-
-    [Graph]: ../../assets/user_guides/components/graph1.png
-
-Note that in the above example we directly inserted the chart into the `figure` argument for the `.py` version. This is also the simplest way to connect your chart to a Pandas `DataFrame` - for other connections, please refer to [this guide](data.md). For the `yaml` version, we simply referred to the [`plotly.express`](https://plotly.com/python/plotly-express/) name by string.
-
-
-???+ info
-
-    When importing Vizro, we automatically set the `plotly` [default template](https://plotly.com/python/templates/#specifying-a-default-themes) to
-    a custom designed template. In case you would like to set the default back, simply run
-    ```py
-    import plotly.io as pio
-    pio.templates.default = "plotly"
-    ```
-    or enter your desired template into any `plotly.express` chart as `template="plotly"` on a case-by-case basis.
-    Note that we do not recommend the above steps for use in dashboards, as other templates will look out-of-sync with overall dashboard design.
+[`Graph`][vizro.models.Graph], [`Table`][vizro.models.Table], [`Card`][vizro.models.Card] or [`Button`][vizro.models.Button].
 
 ## Card
 
diff --git a/vizro-core/docs/pages/user_guides/graph.md b/vizro-core/docs/pages/user_guides/graph.md
new file mode 100755
index 000000000..8f5c91a23
--- /dev/null
+++ b/vizro-core/docs/pages/user_guides/graph.md
@@ -0,0 +1,89 @@
+# How to use graphs
+
+This guide shows you how to use graphs to visualize your data in the dashboard.
+
+The [`Page`][vizro.models.Page] models accepts the `components` argument, where you can enter your visual content e.g.
+[`Graph`][vizro.models.Graph], [`Table`][vizro.models.Table], [`Card`][vizro.models.Card] or [`Button`][vizro.models.Button].
+
+## Graph
+
+The [`Graph`][vizro.models.Graph] model is the most used component in many dashboards, allowing you to visualize data in a variety of ways.
+
+To add a [`Graph`][vizro.models.Graph] to your page, do the following:
+
+- insert the [`Graph`][vizro.models.Graph] model into the `components` argument of the
+[`Page`][vizro.models.Page] model
+- enter any of the currently available charts of the open source library [`plotly.express`](https://plotly.com/python/plotly-express/) into the `figure` argument
+
+!!! note
+
+    In order to use the [`plotly.express`](https://plotly.com/python/plotly-express/) chart in a Vizro dashboard, you need to import it as `import vizro.plotly.express as px`.
+    This leaves any of the [`plotly.express`](https://plotly.com/python/plotly-express/) functionality untouched, but allows _direct insertion_ into the [`Graph`][vizro.models.Graph] model _as is_.
+
+
+
+!!! example "Graph"
+    === "app.py"
+        ```py
+        import vizro.models as vm
+        import vizro.plotly.express as px
+        from vizro import Vizro
+
+        df = px.data.iris()
+
+        page = vm.Page(
+            title="My first page",
+            components=[
+                vm.Graph(
+                    id="my_chart",
+                    figure=px.scatter_matrix(
+                        df, dimensions=["sepal_length", "sepal_width", "petal_length", "petal_width"], color="species"
+                    ),
+                ),
+            ],
+            controls=[vm.Filter(column="species", selector=vm.Dropdown(title="Species"))],
+        )
+
+        dashboard = vm.Dashboard(pages=[page])
+
+        Vizro().build(dashboard).run()
+        ```
+    === "app.yaml"
+        ```yaml
+        # Still requires a .py to register data connector in Data Manager and parse yaml configuration
+        # See from_yaml example
+        pages:
+        - components:
+          - figure:
+              _target_: scatter_matrix
+              color: species
+              data_frame: iris
+              dimensions: ["sepal_length", "sepal_width", "petal_length", "petal_width"]
+            id: my_chart
+            type: graph
+          controls:
+            - column: species
+              type: filter
+              selector:
+                title: Species
+                type: dropdown
+          title: My first page
+        ```
+    === "Result"
+        [![Graph]][Graph]
+
+    [Graph]: ../../assets/user_guides/components/graph1.png
+
+Note that in the above example we directly inserted the chart into the `figure` argument for the `.py` version. This is also the simplest way to connect your chart to a Pandas `DataFrame` - for other connections, please refer to [this guide on data connections](data.md). For the `yaml` version, we simply referred to the [`plotly.express`](https://plotly.com/python/plotly-express/) name by string.
+
+
+??? info "Vizro automatically sets the plotly default template"
+
+    When importing Vizro, we automatically set the `plotly` [default template](https://plotly.com/python/templates/#specifying-a-default-themes) to
+    a custom designed template. In case you would like to set the default back, simply run
+    ```py
+    import plotly.io as pio
+    pio.templates.default = "plotly"
+    ```
+    or enter your desired template into any `plotly.express` chart as `template="plotly"` on a case-by-case basis.
+    Note that we do not recommend the above steps for use in dashboards, as other templates will look out-of-sync with overall dashboard design.
diff --git a/vizro-core/docs/pages/user_guides/table.md b/vizro-core/docs/pages/user_guides/table.md
new file mode 100755
index 000000000..81304b2b7
--- /dev/null
+++ b/vizro-core/docs/pages/user_guides/table.md
@@ -0,0 +1,287 @@
+# How to use tables
+
+This guide shows you how to use tables to visualize your data in the dashboard.
+
+The [`Page`][vizro.models.Page] models accepts the `components` argument, where you can enter your visual content e.g.
+[`Graph`][vizro.models.Graph], [`Table`][vizro.models.Table], [`Card`][vizro.models.Card] or [`Button`][vizro.models.Button].
+
+## Table
+
+The [`Table`][vizro.models.Table] model allows you to visualize data in a tabular format.
+
+To add a [`Table`][vizro.models.Table] to your page, do the following:
+
+- insert the [`Table`][vizro.models.Table] model into the `components` argument of the
+[`Page`][vizro.models.Page] model
+- enter any of the currently available table functions
+
+See below for an overview of currently supported table functions.
+
+### Dash DataTable
+
+The [Dash DataTable](https://dash.plotly.com/datatable) is an interactive table component designed for viewing, editing, and exploring large datasets.
+
+You can use the [Dash DataTable](https://dash.plotly.com/datatable) in Vizro by importing
+```py
+from vizro.tables import dash_data_table
+```
+The Vizro version of this table differs in one way from the original table: it requires the user to provide a pandas dataframe as source of data.
+This must be entered under the argument `data_frame`.
+All other [parameters of the Dash DataTable](https://dash.plotly.com/datatable/reference) can be entered as keyword arguments. Note that we are
+setting some defaults for some of the arguments to help with styling.
+
+!!! example "Dash DataTable"
+    === "app.py"
+        ```py
+        import vizro.models as vm
+        import vizro.plotly.express as px
+        from vizro import Vizro
+        from vizro.tables import dash_data_table
+
+        df = px.data.gapminder().query("year == 2007")
+
+        page = vm.Page(
+            title="Example of a Dash DataTable",
+            components=[
+                vm.Table(id="table", title="Dash DataTable", figure=dash_data_table(data_frame=df)),
+            ],
+            controls=[vm.Filter(column="continent")],
+        )
+        dashboard = vm.Dashboard(pages=[page])
+
+        Vizro().build(dashboard).run()
+        ```
+    === "app.yaml"
+        ```yaml
+        # Still requires a .py to register data connector in Data Manager and parse yaml configuration
+        # See from_yaml example
+        pages:
+        - components:
+          - figure:
+              _target_: dash_data_table
+              data_frame: gapminder_2007
+            title: Dash DataTable
+            id: table
+            type: table
+          controls:
+            - column: continent
+              type: filter
+          title: Example of a Dash DataTable
+        ```
+    === "Result"
+        [![Table]][Table]
+
+    [Table]: ../../assets/user_guides/table/table.png
+
+#### Styling/Modifying the Dash DataTable
+
+As mentioned above, all [parameters of the Dash DataTable](https://dash.plotly.com/datatable/reference) can be entered as keyword arguments. Below you can find
+an example of a styled table where some conditional formatting is applied. There are many more ways to alter the table beyond this showcase.
+
+??? example "Styled Dash DataTable"
+    === "app.py"
+        ```py
+        import vizro.models as vm
+        import vizro.plotly.express as px
+        from vizro import Vizro
+        from vizro.tables import dash_data_table
+
+        df = px.data.gapminder().query("year == 2007")
+
+        column_definitions = [
+            {"name": "country", "id": "country", "type": "text", "editable": False},
+            {"name": "continent", "id": "continent", "type": "text"},
+            {"name": "year", "id": "year", "type": "datetime"},
+            {"name": "lifeExp", "id": "lifeExp", "type": "numeric"},
+            {"name": "pop", "id": "pop", "type": "numeric"},
+            {"name": "gdpPercap", "id": "gdpPercap", "type": "numeric"},
+        ]
+
+        style_data_conditional = [
+            {
+                "if": {
+                    "column_id": "year",
+                },
+                "backgroundColor": "dodgerblue",
+                "color": "white",
+            },
+            {"if": {"filter_query": "{lifeExp} < 55", "column_id": "lifeExp"}, "backgroundColor": "#85144b", "color": "white"},
+            {
+                "if": {"filter_query": "{gdpPercap} > 10000", "column_id": "gdpPercap"},
+                "backgroundColor": "green",
+                "color": "white",
+            },
+            {"if": {"column_type": "text"}, "textAlign": "left"},
+            {
+                "if": {"state": "active"},
+                "backgroundColor": "rgba(0, 116, 217, 0.3)",
+                "border": "1px solid rgb(0, 116, 217)",
+            },
+        ]
+
+        style_header_conditional = [{"if": {"column_type": "text"}, "textAlign": "left"}]
+
+        page = vm.Page(
+            title="Example of a styled Dash DataTable",
+            components=[
+                vm.Table(
+                    id="table",
+                    title="Styled table",
+                    figure=dash_data_table(
+                        data_frame=df,
+                        columns=column_definitions,
+                        sort_action="native",
+                        editable=True,
+                        style_data_conditional=style_data_conditional,
+                        style_header_conditional=style_header_conditional,
+                    ),
+                ),
+            ],
+            controls=[vm.Filter(column="continent")],
+        )
+        dashboard = vm.Dashboard(pages=[page])
+
+        Vizro().build(dashboard).run()
+        ```
+    === "app.yaml"
+        ```yaml
+        # Still requires a .py to register data connector in Data Manager and parse yaml configuration
+        # See from_yaml example
+        pages:
+          - components:
+              - figure:
+                  _target_: dash_data_table
+                  data_frame: gapminder_2007
+                  sort_action: native
+                  editable: true
+                  columns:
+                    - name: country
+                      id: country
+                      type: text
+                      editable: false
+                    - name: continent
+                      id: continent
+                      type: text
+                    - name: year
+                      id: year
+                      type: datetime
+                    - name: lifeExp
+                      id: lifeExp
+                      type: numeric
+                    - name: pop
+                      id: pop
+                      type: numeric
+                    - name: gdpPercap
+                      id: gdpPercap
+                      type: numeric
+                  style_data_conditional:
+                    - if:
+                        column_id: year
+                      backgroundColor: dodgerblue
+                      color: white
+                    - if:
+                        filter_query: "{lifeExp} < 55"
+                        column_id: lifeExp
+                      backgroundColor: "#85144b"
+                      color: white
+                    - if:
+                        filter_query: "{gdpPercap} > 10000"
+                        column_id: gdpPercap
+                      backgroundColor: green
+                      color: white
+                    - if:
+                        column_type: text
+                      textAlign: left
+                    - if:
+                        state: active
+                      backgroundColor: rgba(0, 116, 217, 0.3)
+                      border: 1px solid rgb(0, 116, 217)
+                id: table
+                type: table
+            controls:
+              - column: continent
+                type: filter
+            title: Dash DataTable
+
+        ```
+    === "Result"
+        [![Table2]][Table2]
+
+    [Table2]: ../../assets/user_guides/table/styled_table.png
+
+#### Custom Table
+
+In case you want to add custom logic to a Dash DataTable, e.g. when requiring computations that can be controlled by parameters, it is possible to
+create a custom Dash DataTable in Vizro.
+
+For this, similar to how one would create a [custom chart](../user_guides/custom_charts.md), simply do the following:
+
+- define a function that returns a  `dash_table.DataTable` object
+- decorate it with the `@capture("table")` decorator
+- the function must accept a `data_frame` argument (of type `pandas.DataFrame`)
+- the table should be derived from and require only one `pandas.DataFrame` (e.g. any further dataframes added through other arguments will not react to dashboard components such as `Filter`)
+
+
+The following example shows a possible version of a custom table. In this case the argument `chosen_columns` was added, which you can control with a parameter:
+
+??? example "Custom Dash DataTable"
+    === "app.py"
+        ```py
+        from typing import List
+
+        from dash import dash_table
+
+        import vizro.models as vm
+        import vizro.plotly.express as px
+        from vizro import Vizro
+        from vizro.models.types import capture
+
+        df = px.data.gapminder().query("year == 2007")
+
+
+        @capture("table")
+        def my_custom_table(data_frame=None, chosen_columns: List[str] = None):
+            """Custom table."""
+            columns = [{"name": i, "id": i} for i in chosen_columns]
+            defaults = {
+                "style_as_list_view": True,
+                "style_data": {"border_bottom": "1px solid var(--border-subtle-alpha-01)", "height": "40px"},
+                "style_header": {
+                    "border_bottom": "1px solid var(--state-overlays-selected-hover)",
+                    "border_top": "1px solid var(--main-container-bg-color)",
+                    "height": "32px",
+                },
+            }
+            return dash_table.DataTable(data=data_frame.to_dict("records"), columns=columns, **defaults)
+
+
+        page = vm.Page(
+            title="Example of a custom Dash DataTable",
+            components=[
+                vm.Table(
+                    id="custom_table",
+                    title="Custom Dash DataTable",
+                    figure=my_custom_table(
+                        data_frame=df, chosen_columns=["country", "continent", "lifeExp", "pop", "gdpPercap"]
+                    ),
+                ),
+            ],
+            controls=[
+                vm.Parameter(
+                    targets=["custom_table.chosen_columns"],
+                    selector=vm.Dropdown(title="Choose columns", options=df.columns.to_list(), multi=True),
+                )
+            ],
+        )
+        dashboard = vm.Dashboard(pages=[page])
+
+        Vizro().build(dashboard).run()
+        ```
+    === "app.yaml"
+        ```yaml
+        # Custom tables are currently only possible via python configuration
+        ```
+    === "Result"
+        [![Table3]][Table3]
+
+    [Table3]: ../../assets/user_guides/table/custom_table.png
diff --git a/vizro-core/mkdocs.yml b/vizro-core/mkdocs.yml
index 5f925659f..65a9ba483 100644
--- a/vizro-core/mkdocs.yml
+++ b/vizro-core/mkdocs.yml
@@ -12,7 +12,10 @@ nav:
           - Pages: pages/user_guides/pages.md
           - Run Methods: pages/user_guides/run.md
       - Components:
-          - Charts, Cards, Buttons: pages/user_guides/components.md
+          - Graphs: pages/user_guides/graph.md
+          - Tables: pages/user_guides/table.md
+          - Cards & Buttons: pages/user_guides/components.md
+
       - Controls:
           - Filters: pages/user_guides/filters.md
           - Parameters: pages/user_guides/parameters.md
diff --git a/vizro-core/schemas/0.1.5.json b/vizro-core/schemas/0.1.5.json
index a1cf7a7fc..0e2e91e5d 100644
--- a/vizro-core/schemas/0.1.5.json
+++ b/vizro-core/schemas/0.1.5.json
@@ -159,6 +159,33 @@
       },
       "additionalProperties": false
     },
+    "Table": {
+      "title": "Table",
+      "description": "Wrapper for table components to visualize in dashboard.\n\nArgs:\n    type (Literal[\"table\"]): Defaults to `\"table\"`.\n    table (CapturedCallable): Table like object to be displayed. Current choices include:\n        [`dash_table.DataTable`](https://dash.plotly.com/datatable).\n    actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "title": "Id",
+          "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.",
+          "type": "string"
+        },
+        "type": {
+          "title": "Type",
+          "default": "table",
+          "enum": ["table"],
+          "type": "string"
+        },
+        "actions": {
+          "title": "Actions",
+          "default": [],
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Action"
+          }
+        }
+      },
+      "additionalProperties": false
+    },
     "Layout": {
       "title": "Layout",
       "description": "Grid specification to place chart/components on the [`Page`][vizro.models.Page].\n\nArgs:\n    grid (List[List[int]]): Grid specification to arrange components on screen.\n    row_gap (str): Gap between rows in px. Defaults to `\"12px\"`.\n    col_gap (str): Gap between columns in px. Defaults to `\"12px\"`.\n    row_min_height (str): Minimum row height in px. Defaults to `\"0px\"`.\n    col_min_width (str): Minimum column width in px. Defaults to `\"0px\"`.",
@@ -628,7 +655,7 @@
     },
     "Filter": {
       "title": "Filter",
-      "description": "Filter the data supplied to `targets` on the [`Page`][vizro.models.Page].\n\nExamples:\n    >>> print(repr(Filter(column=\"species\")))\n\nArgs:\n    type (Literal[\"filter\"]): Defaults to `\"filter\"`.\n    column (str): Column of `DataFrame` to filter.\n    targets (List[str]): Target component to be affected by filter. If none are given then target all components on\n        the page that use `column`.\n    selector (Optional[SelectorType]): See [SelectorType][vizro.models.types.SelectorType]. Defaults to `None`.",
+      "description": "Filter the data supplied to `targets` on the [`Page`][vizro.models.Page].\n\nExamples:\n    >>> print(repr(Filter(column=\"species\")))\n\nArgs:\n    type (Literal[\"filter\"]): Defaults to `\"filter\"`.\n    column (str): Column of `DataFrame` to filter.\n    targets (List[ModelID]): Target component to be affected by filter. If none are given then target all components\n        on the page that use `column`.\n    selector (Optional[SelectorType]): See [SelectorType][vizro.models.types.SelectorType]. Defaults to `None`.",
       "type": "object",
       "properties": {
         "id": {
@@ -796,7 +823,8 @@
               "mapping": {
                 "button": "#/definitions/Button",
                 "card": "#/definitions/Card",
-                "graph": "#/definitions/Graph"
+                "graph": "#/definitions/Graph",
+                "table": "#/definitions/Table"
               }
             },
             "oneOf": [
@@ -808,6 +836,9 @@
               },
               {
                 "$ref": "#/definitions/Graph"
+              },
+              {
+                "$ref": "#/definitions/Table"
               }
             ]
           }
diff --git a/vizro-core/schemas/0.1.6.dev0.json b/vizro-core/schemas/0.1.6.dev0.json
index a1cf7a7fc..95b345bcb 100644
--- a/vizro-core/schemas/0.1.6.dev0.json
+++ b/vizro-core/schemas/0.1.6.dev0.json
@@ -159,6 +159,38 @@
       },
       "additionalProperties": false
     },
+    "Table": {
+      "title": "Table",
+      "description": "Wrapper for table components to visualize in dashboard.\n\nArgs:\n    type (Literal[\"table\"]): Defaults to `\"table\"`.\n    figure (CapturedCallable): Table like object to be displayed. Current choices include:\n        [`dash_table.DataTable`](https://dash.plotly.com/datatable).\n    title (str): Title of the table. Defaults to `None`.\n    actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.",
+      "type": "object",
+      "properties": {
+        "id": {
+          "title": "Id",
+          "description": "ID to identify model. Must be unique throughout the whole dashboard.When no ID is chosen, ID will be automatically generated.",
+          "type": "string"
+        },
+        "type": {
+          "title": "Type",
+          "default": "table",
+          "enum": ["table"],
+          "type": "string"
+        },
+        "title": {
+          "title": "Title",
+          "description": "Title of the table",
+          "type": "string"
+        },
+        "actions": {
+          "title": "Actions",
+          "default": [],
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Action"
+          }
+        }
+      },
+      "additionalProperties": false
+    },
     "Layout": {
       "title": "Layout",
       "description": "Grid specification to place chart/components on the [`Page`][vizro.models.Page].\n\nArgs:\n    grid (List[List[int]]): Grid specification to arrange components on screen.\n    row_gap (str): Gap between rows in px. Defaults to `\"12px\"`.\n    col_gap (str): Gap between columns in px. Defaults to `\"12px\"`.\n    row_min_height (str): Minimum row height in px. Defaults to `\"0px\"`.\n    col_min_width (str): Minimum column width in px. Defaults to `\"0px\"`.",
@@ -628,7 +660,7 @@
     },
     "Filter": {
       "title": "Filter",
-      "description": "Filter the data supplied to `targets` on the [`Page`][vizro.models.Page].\n\nExamples:\n    >>> print(repr(Filter(column=\"species\")))\n\nArgs:\n    type (Literal[\"filter\"]): Defaults to `\"filter\"`.\n    column (str): Column of `DataFrame` to filter.\n    targets (List[str]): Target component to be affected by filter. If none are given then target all components on\n        the page that use `column`.\n    selector (Optional[SelectorType]): See [SelectorType][vizro.models.types.SelectorType]. Defaults to `None`.",
+      "description": "Filter the data supplied to `targets` on the [`Page`][vizro.models.Page].\n\nExamples:\n    >>> print(repr(Filter(column=\"species\")))\n\nArgs:\n    type (Literal[\"filter\"]): Defaults to `\"filter\"`.\n    column (str): Column of `DataFrame` to filter.\n    targets (List[ModelID]): Target component to be affected by filter. If none are given then target all components\n        on the page that use `column`.\n    selector (Optional[SelectorType]): See [SelectorType][vizro.models.types.SelectorType]. Defaults to `None`.",
       "type": "object",
       "properties": {
         "id": {
@@ -796,7 +828,8 @@
               "mapping": {
                 "button": "#/definitions/Button",
                 "card": "#/definitions/Card",
-                "graph": "#/definitions/Graph"
+                "graph": "#/definitions/Graph",
+                "table": "#/definitions/Table"
               }
             },
             "oneOf": [
@@ -808,6 +841,9 @@
               },
               {
                 "$ref": "#/definitions/Graph"
+              },
+              {
+                "$ref": "#/definitions/Table"
               }
             ]
           }
diff --git a/vizro-core/schemas/generate.py b/vizro-core/schemas/generate.py
index a171a3129..c7b9ad381 100644
--- a/vizro-core/schemas/generate.py
+++ b/vizro-core/schemas/generate.py
@@ -22,4 +22,4 @@
     print("JSON schema is up to date.")  # noqa: T201
 else:
     schema_path.write_text(schema_json)
-    subprocess.run(f"pre-commit run prettier --files {schema_path}", shell=True, stdout=subprocess.DEVNULL)  # nosec
+    subprocess.run("hatch run lint", shell=True, stdout=subprocess.DEVNULL)  # nosec
diff --git a/vizro-core/src/vizro/actions/_actions_utils.py b/vizro-core/src/vizro/actions/_actions_utils.py
index 7e35deee6..f1b45b284 100644
--- a/vizro-core/src/vizro/actions/_actions_utils.py
+++ b/vizro-core/src/vizro/actions/_actions_utils.py
@@ -21,7 +21,7 @@
 
 class CallbackTriggerDict(TypedDict):  # shortened as 'ctd'
     id: ModelID  # the component ID. If it`s a pattern matching ID, it will be a dict.
-    property: Literal["clickData", "value", "n_clicks"]  #  the component property used in the callback.
+    property: Literal["clickData", "value", "n_clicks", "active_cell"]  #  the component property used in the callback.
     value: Optional[Any]  # the value of the component property at the time the callback was fired.
     str_id: str  # for pattern matching IDs, it`s the stringified dict ID with no white spaces.
     triggered: bool  #  a boolean indicating whether this input triggered the callback.
@@ -113,17 +113,21 @@ def _update_nested_graph_properties(graph_config: Dict[str, Any], dot_separated_
     return graph_config
 
 
-def _get_parametrized_config(targets: List[str], parameters: List[CallbackTriggerDict]) -> Dict[str, Dict[str, Any]]:
+def _get_parametrized_config(
+    targets: List[ModelID], parameters: List[CallbackTriggerDict]
+) -> Dict[ModelID, Dict[str, Any]]:
     parameterized_config = {}
     for target in targets:
         # TODO - avoid calling _captured_callable. Once we have done this we can remove _arguments from
         #  CapturedCallable entirely.
-        graph_config = deepcopy(model_manager[target].figure._arguments)  # type: ignore[index, attr-defined]
+        graph_config = deepcopy(model_manager[target].figure._arguments)  # type: ignore[attr-defined]
         if "data_frame" in graph_config:
             graph_config.pop("data_frame")
 
         for ctd in parameters:
-            selector_value = ctd["value"]
+            selector_value = ctd[
+                "value"
+            ]  # TODO: needs to be refactored so that it is independent of implementation details
             if hasattr(selector_value, "__iter__") and ALL_OPTION in selector_value:  # type: ignore[operator]
                 selector: SelectorType = model_manager[ctd["id"]]
                 selector_value = selector.options
@@ -148,10 +152,10 @@ def _get_parametrized_config(targets: List[str], parameters: List[CallbackTrigge
 
 # Helper functions used in pre-defined actions ----
 def _get_filtered_data(
-    targets: List[str],
+    targets: List[ModelID],
     ctds_filters: List[CallbackTriggerDict],
     ctds_filter_interaction: List[CallbackTriggerDict],
-) -> Dict[str, pd.DataFrame]:
+) -> Dict[ModelID, pd.DataFrame]:
     filtered_data = {}
     for target in targets:
         data_frame = data_manager._get_component_data(target)
@@ -177,8 +181,8 @@ def _get_modified_page_charts(
     ctds_filter_interaction: List[CallbackTriggerDict],
     ctds_parameters: List[CallbackTriggerDict],
     ctd_theme: CallbackTriggerDict,
-    targets: Optional[List[str]] = None,
-) -> Dict[str, Any]:
+    targets: Optional[List[ModelID]] = None,
+) -> Dict[ModelID, Any]:
     if not targets:
         targets = []
     filtered_data = _get_filtered_data(
@@ -192,12 +196,12 @@ def _get_modified_page_charts(
         parameters=ctds_parameters,
     )
 
-    outputs = {
-        target: model_manager[target](  # type: ignore[index, operator]
-            data_frame=filtered_data[target],
-            **parameterized_config[target],
-        ).update_layout(template="vizro_dark" if ctd_theme["value"] else "vizro_light")
-        for target in targets
-    }
+    outputs = {}
+    for target in targets:
+        outputs[target] = model_manager[target](  # type: ignore[operator]
+            data_frame=filtered_data[target], **parameterized_config[target]
+        )
+        if hasattr(outputs[target], "update_layout"):
+            outputs[target].update_layout(template="vizro_dark" if ctd_theme["value"] else "vizro_light")
 
     return outputs
diff --git a/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py b/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py
index 2954e1676..2d6b92c39 100644
--- a/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py
+++ b/vizro-core/src/vizro/actions/_callback_mapping/_callback_mapping_utils.py
@@ -89,7 +89,7 @@ def _get_inputs_of_controls(action_id: ModelID, control_type: ControlType) -> Li
     return [
         State(
             component_id=control.selector.id,
-            component_property="value",
+            component_property=control.selector._input_property,
         )
         for control in page.controls
         if isinstance(control, control_type)
@@ -107,7 +107,7 @@ def _get_inputs_of_chart_interactions(
     return [
         State(
             component_id=_get_triggered_model(action_id=ModelID(str(action.id))).id,
-            component_property="clickData",
+            component_property="clickData",  # TODO: needs to be refactored to abstract implementation detail
         )
         for action in chart_interactions_on_page
     ]
@@ -145,6 +145,10 @@ def _get_action_callback_inputs(action_id: ModelID) -> Dict[str, List[State]]:
 def _get_action_callback_outputs(action_id: ModelID) -> Dict[str, Output]:
     """Creates mapping of target names and their Output."""
     action_function = model_manager[action_id].function._function  # type: ignore[attr-defined]
+    # The right solution for mypy here is to not e.g. define new attributes on the base but instead to get mypy to
+    # recognize that model_manager[action_id] is of type Action and hence has the function attribute.
+    # Ideally model_manager.__getitem__ would handle this itself, possibly with suitable use of a cast.
+    # If not then we can do the cast to Action at the point of consumption here to avoid needing mypy ignores.
 
     try:
         targets = model_manager[action_id].function["targets"]  # type: ignore[attr-defined]
@@ -160,7 +164,7 @@ def _get_action_callback_outputs(action_id: ModelID) -> Dict[str, Output]:
     return {
         target: Output(
             component_id=target,
-            component_property="figure",
+            component_property=model_manager[target]._output_property,  # type: ignore[attr-defined]
             allow_duplicate=True,
         )
         for target in targets
diff --git a/vizro-core/src/vizro/actions/_filter_action.py b/vizro-core/src/vizro/actions/_filter_action.py
index 4516a96dd..16b29b39e 100644
--- a/vizro-core/src/vizro/actions/_filter_action.py
+++ b/vizro-core/src/vizro/actions/_filter_action.py
@@ -8,16 +8,17 @@
 from vizro.actions._actions_utils import (
     _get_modified_page_charts,
 )
+from vizro.managers._model_manager import ModelID
 from vizro.models.types import capture
 
 
 @capture("action")
 def _filter(
     filter_column: str,
-    targets: List[str],
+    targets: List[ModelID],
     filter_function: Callable[[pd.Series, Any], pd.Series],
     **inputs: Dict[str, Any],
-) -> Dict[str, Any]:
+) -> Dict[ModelID, Any]:
     """Filters targeted charts/components on page by interaction with `Filter` control.
 
     Args:
diff --git a/vizro-core/src/vizro/actions/_on_page_load_action.py b/vizro-core/src/vizro/actions/_on_page_load_action.py
index 28e513b31..268fbfe60 100644
--- a/vizro-core/src/vizro/actions/_on_page_load_action.py
+++ b/vizro-core/src/vizro/actions/_on_page_load_action.py
@@ -13,7 +13,7 @@
 
 
 @capture("action")
-def _on_page_load(page_id: ModelID, **inputs: Dict[str, Any]) -> Dict[str, Any]:
+def _on_page_load(page_id: ModelID, **inputs: Dict[str, Any]) -> Dict[ModelID, Any]:
     """Applies controls to charts on page once the page is opened (or refreshed).
 
     Args:
diff --git a/vizro-core/src/vizro/actions/_parameter_action.py b/vizro-core/src/vizro/actions/_parameter_action.py
index caf9faebf..14f492b35 100644
--- a/vizro-core/src/vizro/actions/_parameter_action.py
+++ b/vizro-core/src/vizro/actions/_parameter_action.py
@@ -7,12 +7,13 @@
 from vizro.actions._actions_utils import (
     _get_modified_page_charts,
 )
+from vizro.managers._model_manager import ModelID
 from vizro.models.types import capture
 
 
 # TODO - consider using dash.Patch() for parameter action
 @capture("action")
-def _parameter(targets: List[str], **inputs: Dict[str, Any]) -> Dict[str, Any]:
+def _parameter(targets: List[str], **inputs: Dict[str, Any]) -> Dict[ModelID, Any]:
     """Modifies parameters of targeted charts/components on page.
 
     Args:
@@ -23,10 +24,10 @@ def _parameter(targets: List[str], **inputs: Dict[str, Any]) -> Dict[str, Any]:
     Returns:
         Dict mapping target component ids to modified charts/components e.g. {'my_scatter': Figure({})}
     """
-    targets = [target.split(".")[0] for target in targets]
+    target_ids: List[ModelID] = [target.split(".")[0] for target in targets]  # type: ignore[misc]
 
     return _get_modified_page_charts(
-        targets=targets,
+        targets=target_ids,
         ctds_filter=ctx.args_grouping["filters"],
         ctds_filter_interaction=ctx.args_grouping["filter_interaction"],
         ctds_parameters=ctx.args_grouping["parameters"],
diff --git a/vizro-core/src/vizro/actions/export_data_action.py b/vizro-core/src/vizro/actions/export_data_action.py
index 5af593235..afaded8cf 100644
--- a/vizro-core/src/vizro/actions/export_data_action.py
+++ b/vizro-core/src/vizro/actions/export_data_action.py
@@ -9,12 +9,13 @@
     _get_filtered_data,
 )
 from vizro.managers import model_manager
+from vizro.managers._model_manager import ModelID
 from vizro.models.types import capture
 
 
 @capture("action")
 def export_data(
-    targets: Optional[List[str]] = None,
+    targets: Optional[List[ModelID]] = None,
     file_format: Literal["csv", "xlsx"] = "csv",
     **inputs: Dict[str, Any],
 ) -> Dict[str, Any]:
@@ -40,7 +41,7 @@ def export_data(
             if isinstance(output["id"], dict) and output["id"]["type"] == "download-dataframe"
         ]
     for target in targets:
-        if target not in model_manager:  # type: ignore[operator]
+        if target not in model_manager:
             raise ValueError(f"Component '{target}' does not exist.")
 
     data_frames = _get_filtered_data(
@@ -50,7 +51,7 @@ def export_data(
     )
 
     callback_outputs = {}
-    for _, target_id in enumerate(targets):
+    for target_id in targets:
         if file_format == "csv":
             writer = data_frames[target_id].to_csv
         elif file_format == "xlsx":
diff --git a/vizro-core/src/vizro/actions/filter_interaction_action.py b/vizro-core/src/vizro/actions/filter_interaction_action.py
index 51321a8ab..00e30a64c 100644
--- a/vizro-core/src/vizro/actions/filter_interaction_action.py
+++ b/vizro-core/src/vizro/actions/filter_interaction_action.py
@@ -7,14 +7,15 @@
 from vizro.actions._actions_utils import (
     _get_modified_page_charts,
 )
+from vizro.managers._model_manager import ModelID
 from vizro.models.types import capture
 
 
 @capture("action")
 def filter_interaction(
-    targets: Optional[List[str]] = None,
+    targets: Optional[List[ModelID]] = None,
     **inputs: Dict[str, Any],
-) -> Dict[str, Any]:
+) -> Dict[ModelID, Any]:
     """Filters targeted charts/components on page by clicking on data points of the source chart.
 
     To set up filtering on specific columns of the target chart(s), include these columns in the 'custom_data'
diff --git a/vizro-core/src/vizro/managers/_model_manager.py b/vizro-core/src/vizro/managers/_model_manager.py
index 160427e4c..cc1b9984d 100644
--- a/vizro-core/src/vizro/managers/_model_manager.py
+++ b/vizro-core/src/vizro/managers/_model_manager.py
@@ -19,8 +19,6 @@
 class DuplicateIDError(ValueError):
     """Useful for providing a more explicit error message when a model has id set automatically, e.g. Page."""
 
-    pass
-
 
 class ModelManager:
     def __init__(self):
diff --git a/vizro-core/src/vizro/models/__init__.py b/vizro-core/src/vizro/models/__init__.py
index 776a4bfab..e7a517220 100644
--- a/vizro-core/src/vizro/models/__init__.py
+++ b/vizro-core/src/vizro/models/__init__.py
@@ -1,7 +1,7 @@
 # Keep this import at the top to avoid circular imports since it's used in every model.
 from ._base import VizroBaseModel  # noqa: I001
 from ._action import Action
-from ._components import Card, Graph
+from ._components import Card, Graph, Table
 from ._components.form import Button, Checklist, Dropdown, RadioItems, RangeSlider, Slider
 from ._controls import Filter, Parameter
 from ._navigation.navigation import Navigation
@@ -9,7 +9,7 @@
 from ._layout import Layout
 from ._page import Page
 
-Page.update_forward_refs(Button=Button, Card=Card, Filter=Filter, Graph=Graph, Parameter=Parameter)
+Page.update_forward_refs(Button=Button, Card=Card, Filter=Filter, Graph=Graph, Parameter=Parameter, Table=Table)
 Dashboard.update_forward_refs(Page=Page, Navigation=Navigation)
 
 # Please keep alphabetically ordered
@@ -29,5 +29,6 @@
     "RadioItems",
     "RangeSlider",
     "Slider",
+    "Table",
     "VizroBaseModel",
 ]
diff --git a/vizro-core/src/vizro/models/_components/__init__.py b/vizro-core/src/vizro/models/_components/__init__.py
index fb92f705d..aaa15d36f 100644
--- a/vizro-core/src/vizro/models/_components/__init__.py
+++ b/vizro-core/src/vizro/models/_components/__init__.py
@@ -2,5 +2,6 @@
 from vizro.models._components.button import Button
 from vizro.models._components.card import Card
 from vizro.models._components.graph import Graph
+from vizro.models._components.table import Table
 
-__all__ = ["Button", "Card", "Graph"]
+__all__ = ["Button", "Card", "Graph", "Table"]
diff --git a/vizro-core/src/vizro/models/_components/_components_utils.py b/vizro-core/src/vizro/models/_components/_components_utils.py
new file mode 100644
index 000000000..6f6e7c275
--- /dev/null
+++ b/vizro-core/src/vizro/models/_components/_components_utils.py
@@ -0,0 +1,33 @@
+import logging
+
+from vizro.managers import data_manager
+
+logger = logging.getLogger(__name__)
+
+
+def _process_callable_data_frame(captured_callable, values):
+    data_frame = captured_callable["data_frame"]
+
+    # Enable running e.g. px.scatter("iris") from the Python API and specification of "data_frame": "iris" through JSON.
+    # In these cases, data already exists in the data manager and just needs to be linked to the component.
+    if isinstance(data_frame, str):
+        data_manager._add_component(values["id"], data_frame)
+        return captured_callable
+
+    # Standard case for px.scatter(df: pd.DataFrame).
+    # Extract dataframe from the captured function and put it into the data manager.
+    dataset_name = str(id(data_frame))
+
+    logger.debug("Adding data to data manager for Figure with id %s", values["id"])
+    # If the dataset already exists in the data manager then it's not a problem, it just means that we don't need
+    # to duplicate it. Just log the exception for debugging purposes.
+    try:
+        data_manager[dataset_name] = data_frame
+    except ValueError as exc:
+        logger.debug(exc)
+
+    data_manager._add_component(values["id"], dataset_name)
+
+    # No need to keep the data in the captured function any more so remove it to save memory.
+    del captured_callable["data_frame"]
+    return captured_callable
diff --git a/vizro-core/src/vizro/models/_components/form/checklist.py b/vizro-core/src/vizro/models/_components/form/checklist.py
index 3bb4aacff..8fec2870e 100644
--- a/vizro-core/src/vizro/models/_components/form/checklist.py
+++ b/vizro-core/src/vizro/models/_components/form/checklist.py
@@ -1,7 +1,7 @@
 from typing import List, Literal, Optional
 
 from dash import dcc, html
-from pydantic import Field, root_validator, validator
+from pydantic import Field, PrivateAttr, root_validator, validator
 
 from vizro.models import Action, VizroBaseModel
 from vizro.models._action._actions_chain import _action_validator_factory
@@ -27,6 +27,9 @@ class Checklist(VizroBaseModel):
     title: Optional[str] = Field(None, description="Title to be displayed")
     actions: List[Action] = []
 
+    # Component properties for actions and interactions
+    _input_property: str = PrivateAttr("value")
+
     # Re-used validators
     _set_actions = _action_validator_factory("value")
     _validate_options = root_validator(allow_reuse=True, pre=True)(validate_options_dict)
diff --git a/vizro-core/src/vizro/models/_components/form/dropdown.py b/vizro-core/src/vizro/models/_components/form/dropdown.py
index 4b8e8014a..5db1a375d 100755
--- a/vizro-core/src/vizro/models/_components/form/dropdown.py
+++ b/vizro-core/src/vizro/models/_components/form/dropdown.py
@@ -1,7 +1,7 @@
 from typing import List, Literal, Optional, Union
 
 from dash import dcc, html
-from pydantic import Field, root_validator, validator
+from pydantic import Field, PrivateAttr, root_validator, validator
 
 from vizro.models import Action, VizroBaseModel
 from vizro.models._action._actions_chain import _action_validator_factory
@@ -31,6 +31,9 @@ class Dropdown(VizroBaseModel):
     title: Optional[str] = Field(None, description="Title to be displayed")
     actions: List[Action] = []
 
+    # Component properties for actions and interactions
+    _input_property: str = PrivateAttr("value")
+
     # Re-used validators
     _set_actions = _action_validator_factory("value")
     _validate_options = root_validator(allow_reuse=True, pre=True)(validate_options_dict)
diff --git a/vizro-core/src/vizro/models/_components/form/radio_items.py b/vizro-core/src/vizro/models/_components/form/radio_items.py
index cfccf333e..57e460206 100644
--- a/vizro-core/src/vizro/models/_components/form/radio_items.py
+++ b/vizro-core/src/vizro/models/_components/form/radio_items.py
@@ -1,7 +1,7 @@
 from typing import List, Literal, Optional
 
 from dash import dcc, html
-from pydantic import Field, root_validator, validator
+from pydantic import Field, PrivateAttr, root_validator, validator
 
 from vizro.models import Action, VizroBaseModel
 from vizro.models._action._actions_chain import _action_validator_factory
@@ -28,6 +28,9 @@ class RadioItems(VizroBaseModel):
     title: Optional[str] = Field(None, description="Title to be displayed")
     actions: List[Action] = []
 
+    # Component properties for actions and interactions
+    _input_property: str = PrivateAttr("value")
+
     # Re-used validators
     _set_actions = _action_validator_factory("value")
     _validate_options = root_validator(allow_reuse=True, pre=True)(validate_options_dict)
diff --git a/vizro-core/src/vizro/models/_components/form/range_slider.py b/vizro-core/src/vizro/models/_components/form/range_slider.py
index fc38daf75..64326c711 100644
--- a/vizro-core/src/vizro/models/_components/form/range_slider.py
+++ b/vizro-core/src/vizro/models/_components/form/range_slider.py
@@ -1,7 +1,7 @@
 from typing import Dict, List, Literal, Optional
 
 from dash import ClientsideFunction, Input, Output, State, clientside_callback, dcc, html
-from pydantic import Field, validator
+from pydantic import Field, PrivateAttr, validator
 
 from vizro.models import Action, VizroBaseModel
 from vizro.models._action._actions_chain import _action_validator_factory
@@ -43,6 +43,9 @@ class RangeSlider(VizroBaseModel):
     title: Optional[str] = Field(None, description="Title to be displayed.")
     actions: List[Action] = []
 
+    # Component properties for actions and interactions
+    _input_property: str = PrivateAttr("value")
+
     # Re-used validators
     _validate_max = validator("max", allow_reuse=True)(validate_max)
     _validate_value = validator("value", allow_reuse=True)(validate_slider_value)
diff --git a/vizro-core/src/vizro/models/_components/form/slider.py b/vizro-core/src/vizro/models/_components/form/slider.py
index 291ca5030..5e6607f7e 100644
--- a/vizro-core/src/vizro/models/_components/form/slider.py
+++ b/vizro-core/src/vizro/models/_components/form/slider.py
@@ -1,7 +1,7 @@
 from typing import Dict, List, Literal, Optional
 
 from dash import ClientsideFunction, Input, Output, State, clientside_callback, dcc, html
-from pydantic import Field, validator
+from pydantic import Field, PrivateAttr, validator
 
 from vizro.models import Action, VizroBaseModel
 from vizro.models._action._actions_chain import _action_validator_factory
@@ -41,6 +41,9 @@ class Slider(VizroBaseModel):
     title: Optional[str] = Field(None, description="Title to be displayed.")
     actions: List[Action] = []
 
+    # Component properties for actions and interactions
+    _input_property: str = PrivateAttr("value")
+
     # Re-used validators
     _validate_max = validator("max", allow_reuse=True)(validate_max)
     _validate_value = validator("value", allow_reuse=True)(validate_slider_value)
diff --git a/vizro-core/src/vizro/models/_components/graph.py b/vizro-core/src/vizro/models/_components/graph.py
index d05cc37bf..253007582 100644
--- a/vizro-core/src/vizro/models/_components/graph.py
+++ b/vizro-core/src/vizro/models/_components/graph.py
@@ -3,12 +3,13 @@
 
 from dash import dcc
 from plotly import graph_objects as go
-from pydantic import Field, validator
+from pydantic import Field, PrivateAttr, validator
 
 import vizro.plotly.express as px
 from vizro.managers import data_manager
 from vizro.models import Action, VizroBaseModel
 from vizro.models._action._actions_chain import _action_validator_factory
+from vizro.models._components._components_utils import _process_callable_data_frame
 from vizro.models._models_utils import _log_call
 from vizro.models.types import CapturedCallable
 
@@ -40,36 +41,12 @@ class Graph(VizroBaseModel):
     figure: CapturedCallable = Field(..., import_path=px)
     actions: List[Action] = []
 
+    # Component properties for actions and interactions
+    _output_property: str = PrivateAttr("figure")
+
     # Re-used validators
     _set_actions = _action_validator_factory("clickData")
-
-    @validator("figure")
-    def process_figure_data_frame(cls, figure, values):
-        data_frame = figure["data_frame"]
-
-        # Enable running px.scatter("iris") from the Python API and specification of "data_frame": "iris" through JSON.
-        # In these cases, data already exists in the data manager and just needs to be linked to the component.
-        if isinstance(data_frame, str):
-            data_manager._add_component(values["id"], data_frame)
-            return figure
-
-        # Standard case for px.scatter(df: pd.DataFrame).
-        # Extract dataframe from the captured function and put it into the data manager.
-        dataset_name = str(id(data_frame))
-
-        logger.debug("Adding data to data manager for Graph with id %s", values["id"])
-        # If the dataset already exists in the data manager then it's not a problem, it just means that we don't need
-        # to duplicate it. Just log the exception for debugging purposes.
-        try:
-            data_manager[dataset_name] = data_frame
-        except ValueError as exc:
-            logger.debug(exc)
-
-        data_manager._add_component(values["id"], dataset_name)
-
-        # No need to keep the data in the captured function any more so remove it to save memory.
-        del figure["data_frame"]
-        return figure
+    _validate_callable = validator("figure", allow_reuse=True)(_process_callable_data_frame)
 
     # Convenience wrapper/syntactic sugar.
     def __call__(self, **kwargs):
diff --git a/vizro-core/src/vizro/models/_components/table.py b/vizro-core/src/vizro/models/_components/table.py
new file mode 100644
index 000000000..8215cdacc
--- /dev/null
+++ b/vizro-core/src/vizro/models/_components/table.py
@@ -0,0 +1,65 @@
+import logging
+from typing import List, Literal, Optional
+
+from dash import dash_table, dcc, html
+from pydantic import Field, PrivateAttr, validator
+
+import vizro.tables as vt
+from vizro.managers import data_manager
+from vizro.models import Action, VizroBaseModel
+from vizro.models._action._actions_chain import _action_validator_factory
+from vizro.models._components._components_utils import _process_callable_data_frame
+from vizro.models._models_utils import _log_call
+from vizro.models.types import CapturedCallable
+
+logger = logging.getLogger(__name__)
+
+
+class Table(VizroBaseModel):
+    """Wrapper for table components to visualize in dashboard.
+
+    Args:
+        type (Literal["table"]): Defaults to `"table"`.
+        figure (CapturedCallable): Table like object to be displayed. Current choices include:
+            [`dash_table.DataTable`](https://dash.plotly.com/datatable).
+        title (str): Title of the table. Defaults to `None`.
+        actions (List[Action]): See [`Action`][vizro.models.Action]. Defaults to `[]`.
+    """
+
+    type: Literal["table"] = "table"
+    figure: CapturedCallable = Field(..., import_path=vt, description="Table to be visualized on dashboard")
+    title: Optional[str] = Field(None, description="Title of the table")
+    actions: List[Action] = []
+
+    # Component properties for actions and interactions
+    _output_property: str = PrivateAttr("children")
+
+    # validator
+    set_actions = _action_validator_factory("active_cell")  # type: ignore[pydantic-field]
+    _validate_callable = validator("figure", allow_reuse=True, always=True)(_process_callable_data_frame)
+
+    # Convenience wrapper/syntactic sugar.
+    def __call__(self, **kwargs):
+        kwargs.setdefault("data_frame", data_manager._get_component_data(self.id))  # type: ignore[arg-type]
+        return self.figure(**kwargs)
+
+    # Convenience wrapper/syntactic sugar.
+    def __getitem__(self, arg_name: str):
+        # pydantic discriminated union validation seems to try Table["type"], which throws an error unless we
+        # explicitly redirect it to the correct attribute.
+        if arg_name == "type":
+            return self.type
+        return self.figure[arg_name]
+
+    @_log_call
+    def build(self):
+        return dcc.Loading(
+            html.Div(
+                [
+                    html.H3(self.title, className="table-title") if self.title else None,
+                    html.Div(dash_table.DataTable(), id=self.id),
+                ],
+                className="table-container",
+                id=f"{self.id}_outer",
+            )
+        )
diff --git a/vizro-core/src/vizro/models/_controls/filter.py b/vizro-core/src/vizro/models/_controls/filter.py
index 751d0089d..565a575d1 100644
--- a/vizro-core/src/vizro/models/_controls/filter.py
+++ b/vizro-core/src/vizro/models/_controls/filter.py
@@ -23,6 +23,7 @@
 if TYPE_CHECKING:
     from vizro.models import Page
 
+from vizro.managers._model_manager import ModelID
 
 # TODO: Add temporal when relevant component is available
 SELECTOR_DEFAULTS = {"numerical": RangeSlider, "categorical": Dropdown}
@@ -57,14 +58,14 @@ class Filter(VizroBaseModel):
     Args:
         type (Literal["filter"]): Defaults to `"filter"`.
         column (str): Column of `DataFrame` to filter.
-        targets (List[str]): Target component to be affected by filter. If none are given then target all components on
-            the page that use `column`.
+        targets (List[ModelID]): Target component to be affected by filter. If none are given then target all components
+            on the page that use `column`.
         selector (Optional[SelectorType]): See [SelectorType][vizro.models.types.SelectorType]. Defaults to `None`.
     """
 
     type: Literal["filter"] = "filter"
     column: str = Field(..., description="Column of DataFrame to filter.")
-    targets: List[str] = Field(
+    targets: List[ModelID] = Field(
         [],
         description="Target component to be affected by filter. "
         "If none are given then target all components on the page that use `column`.",
diff --git a/vizro-core/src/vizro/models/_page.py b/vizro-core/src/vizro/models/_page.py
index ae40c2e48..6e57d89ad 100644
--- a/vizro-core/src/vizro/models/_page.py
+++ b/vizro-core/src/vizro/models/_page.py
@@ -98,7 +98,7 @@ def __init__(self, **data):
     @_log_call
     def pre_build(self):
         # TODO: Remove default on page load action if possible
-        if any(isinstance(component, Graph) for component in self.components):
+        if any(hasattr(component, "figure") for component in self.components):
             self.actions = [
                 ActionsChain(
                     id=f"{ON_PAGE_LOAD_ACTION_PREFIX}_{self.id}",
diff --git a/vizro-core/src/vizro/models/types.py b/vizro-core/src/vizro/models/types.py
index ba4ae9eb9..7d4292efc 100644
--- a/vizro-core/src/vizro/models/types.py
+++ b/vizro-core/src/vizro/models/types.py
@@ -29,10 +29,11 @@ class CapturedCallable:
     `functools.partial`.
 
     Ready-to-use `CapturedCallable` instances are provided by Vizro. In this case refer to the user guide on
-    [Charts/Graph][graph] or [Actions][pre-defined-actions] to see available choices.
+    [Charts/Graph][graph], [Table][table] or [Actions][pre-defined-actions] to see available choices.
 
     (Advanced) In case you would like to create your own `CapturedCallable`, please refer to the user guide on
-    [custom charts](../user_guides/custom_charts.md) or [custom actions][custom-actions].
+    [custom charts](../user_guides/custom_charts.md), [custom tables][custom-table] or
+    [custom actions][custom-actions].
     """
 
     def __init__(self, function, /, *args, **kwargs):
@@ -138,8 +139,9 @@ def _parse_json(
         elif not isinstance(callable_config, dict):
             raise ValueError(
                 "You must provide a valid CapturedCallable object. If you are using a plotly express figure, ensure "
-                "that you are using `import vizro.plotly.express as px`. If you are using a custom figure or action, "
-                "that your function uses the @capture decorator."
+                "that you are using `import vizro.plotly.express as px`. If you are using a table figure, make "
+                "sure you are using `from vizro.tables import dash_data_table`. If you are using a custom figure or "
+                "action, that your function uses the @capture decorator."
             )
 
         # Try to import function given in _target_ from the import_path property of the pydantic field.
@@ -176,11 +178,15 @@ class capture:
     """Captures a function call to create a [`CapturedCallable`][vizro.models.types.CapturedCallable].
 
     This is used to add the functionality required to make graphs and actions work in a dashboard.
-    Typically it should be used as a function decorator. There are two possible modes: `"graph"` and `"action"`.
+    Typically it should be used as a function decorator. There are three possible modes: `"graph"`, `"table"` and
+    `"action"`.
 
     Examples:
         >>> @capture("graph")
-        >>> def plot_function():
+        >>> def graph_function():
+        >>>     ...
+        >>> @capture("table")
+        >>> def table_function():
         >>>     ...
         >>> @capture("action")
         >>> def action_function():
@@ -190,15 +196,16 @@ class capture:
     [custom charts](../user_guides/custom_charts.md).
     """
 
-    def __init__(self, mode: Literal["graph", "action"]):
-        """Instantiates the decorator to capture a function call. Valid modes are "graph" and "action"."""
+    def __init__(self, mode: Literal["graph", "action", "table"]):
+        """Instantiates the decorator to capture a function call. Valid modes are "graph", "table" and "action"."""
         self._mode = mode
 
     def __call__(self, func, /):
         """Produces a CapturedCallable or _DashboardReadyFigure.
 
-        mode="action" gives a CapturedCallable, while mode="graph" gives a _DashboardReadyFigure that contains a
-        CapturedCallable. In both cases, the CapturedCallable is based on func and the provided *args and **kwargs.
+        mode="action" and mode="table" give a CapturedCallable, while mode="graph" gives a _DashboardReadyFigure that
+        contains a CapturedCallable. In both cases, the CapturedCallable is based on func and the provided
+        *args and **kwargs.
         """
         if self._mode == "graph":
             # The more difficult case, where we need to still have a valid plotly figure that renders in a notebook.
@@ -244,8 +251,25 @@ def wrapped(*args, **kwargs):
                 return CapturedCallable(func, *args, **kwargs)
 
             return wrapped
+        elif self._mode == "table":
+
+            @functools.wraps(func)
+            def wrapped(*args, **kwargs):
+                if "data_frame" not in inspect.signature(func).parameters:
+                    raise ValueError(f"{func.__name__} must have data_frame argument to use capture('table').")
+
+                captured_callable: CapturedCallable = CapturedCallable(func, *args, **kwargs)
 
-        raise ValueError("Valid modes of the capture decorator are @capture('graph') and @capture('action').")
+                try:
+                    captured_callable["data_frame"]
+                except KeyError as exc:
+                    raise ValueError(f"{func.__name__} must supply a value to data_frame argument.") from exc
+                return captured_callable
+
+            return wrapped
+        raise ValueError(
+            "Valid modes of the capture decorator are @capture('graph'), @capture('action') or @capture('table')."
+        )
 
 
 # Types used for selector values and options. Note the docstrings here are rendered on the API reference.
@@ -296,14 +320,15 @@ class OptionsDictType(TypedDict):
 [`Parameter`][vizro.models.Parameter]."""
 
 ComponentType = Annotated[
-    Union["Button", "Card", "Graph"],
+    Union["Button", "Card", "Graph", "Table"],
     Field(
         discriminator="type",
         description="Component that makes up part of the layout on the page.",
     ),
 ]
 """Discriminated union. Type of component that makes up part of the layout on the page:
-[`Button`][vizro.models.Button], [`Card`][vizro.models.Card] or [`Graph`][vizro.models.Graph]."""
+[`Button`][vizro.models.Button], [`Card`][vizro.models.Card], [`Table`][vizro.models.Table] or
+[`Graph`][vizro.models.Graph]."""
 
 # Types used for pages values in the Navigation model.
 NavigationPagesType = Annotated[
diff --git a/vizro-core/src/vizro/static/css/dropdown.css b/vizro-core/src/vizro/static/css/dropdown.css
index 4db0b0883..5f5fcca63 100644
--- a/vizro-core/src/vizro/static/css/dropdown.css
+++ b/vizro-core/src/vizro/static/css/dropdown.css
@@ -36,9 +36,15 @@ div.page_container .Select-control {
   height: 32px;
 }
 
+div.page_container .Select.is-focused > .Select-control {
+  background-color: var(--field-enabled);
+}
+
 /* User input */
-div.page_container .Select-input {
+div.page_container .dash-dropdown .Select-input {
+  display: block;
   height: var(--tag-height);
+  margin-left: unset;
 }
 
 div.page_container.Select-input > input {
@@ -139,5 +145,5 @@ wrapper **/
 }
 
 .Select-input > input {
-  padding: 0;
+  padding: 0 !important; /*Required so tags don't jump caused by adding table */
 }
diff --git a/vizro-core/src/vizro/static/css/scroll_bar.css b/vizro-core/src/vizro/static/css/scroll_bar.css
index 6af55c15a..2f1122509 100644
--- a/vizro-core/src/vizro/static/css/scroll_bar.css
+++ b/vizro-core/src/vizro/static/css/scroll_bar.css
@@ -27,3 +27,7 @@
 .component_container::-webkit-scrollbar-thumb {
   border-color: var(--main-container-bg-color);
 }
+
+.table-container::-webkit-scrollbar-thumb {
+  border-color: var(--surfaces-bg-02);
+}
diff --git a/vizro-core/src/vizro/static/css/table.css b/vizro-core/src/vizro/static/css/table.css
new file mode 100644
index 000000000..2f00727c8
--- /dev/null
+++ b/vizro-core/src/vizro/static/css/table.css
@@ -0,0 +1,52 @@
+div.right_side
+  .dash-table-container
+  .dash-spreadsheet-container
+  .dash-spreadsheet-inner
+  th {
+  background-color: var(--main-container-bg-color);
+}
+
+div.right_side
+  .dash-table-container
+  .dash-spreadsheet-container
+  .dash-spreadsheet-inner
+  td {
+  background-color: var(--main-container-bg-color);
+}
+
+div.right_side
+  .dash-table-container
+  .dash-spreadsheet-container
+  .dash-spreadsheet-inner
+  table {
+  --hover: var(--main-container-bg-color);
+}
+
+div.right_side
+  .dash-table-container
+  .dash-spreadsheet-container
+  .dash-spreadsheet-inner
+  td
+  .dash-cell-value.unfocused {
+  color: var(--text-primary);
+  font-size: 14px;
+}
+
+div.right_side
+  .dash-table-container
+  .dash-spreadsheet-container
+  .dash-spreadsheet-inner
+  .column-header-name {
+  color: var(
+    --text-primary
+  ); /* Should be text-secondary when hover color is implemented */
+  font-size: 14px;
+}
+
+.table-container {
+  height: 100%;
+  width: 100%;
+  overflow: auto;
+  padding: 12px;
+  padding-left: 32px;
+}
diff --git a/vizro-core/src/vizro/tables/__init__.py b/vizro-core/src/vizro/tables/__init__.py
new file mode 100644
index 000000000..5a813ad8c
--- /dev/null
+++ b/vizro-core/src/vizro/tables/__init__.py
@@ -0,0 +1,4 @@
+from vizro.tables.dash_table import dash_data_table
+
+# Please keep alphabetically ordered
+__all__ = ["dash_data_table"]
diff --git a/vizro-core/src/vizro/tables/dash_table.py b/vizro-core/src/vizro/tables/dash_table.py
new file mode 100644
index 000000000..f28315834
--- /dev/null
+++ b/vizro-core/src/vizro/tables/dash_table.py
@@ -0,0 +1,35 @@
+"""Module containing the standard implementation of `dash_table.DataTable`."""
+from collections import defaultdict
+from typing import Any, Dict, Mapping
+
+import pandas as pd
+from dash import dash_table
+
+from vizro.models.types import capture
+
+
+def _set_defaults_nested(supplied: Mapping[str, Any], defaults: Mapping[str, Any]) -> Dict[str, Any]:
+    supplied = defaultdict(dict, supplied)
+    for default_key, default_value in defaults.items():
+        if isinstance(default_value, Mapping):
+            supplied[default_key] = _set_defaults_nested(supplied[default_key], default_value)
+        else:
+            supplied.setdefault(default_key, default_value)
+    return dict(supplied)
+
+
+@capture("table")
+def dash_data_table(data_frame: pd.DataFrame, **kwargs):
+    """Standard `dash_table.DataTable`."""
+    defaults = {
+        "columns": [{"name": i, "id": i} for i in data_frame.columns],
+        "style_as_list_view": True,
+        "style_data": {"border_bottom": "1px solid var(--border-subtle-alpha-01)", "height": "40px"},
+        "style_header": {
+            "border_bottom": "1px solid var(--state-overlays-selected-hover)",
+            "border_top": "1px solid var(--main-container-bg-color)",
+            "height": "32px",
+        },
+    }
+    kwargs = _set_defaults_nested(kwargs, defaults)
+    return dash_table.DataTable(data=data_frame.to_dict("records"), **kwargs)
diff --git a/vizro-core/tests/unit/vizro/conftest.py b/vizro-core/tests/unit/vizro/conftest.py
index 3570e6dd9..ba3cff2bd 100644
--- a/vizro-core/tests/unit/vizro/conftest.py
+++ b/vizro-core/tests/unit/vizro/conftest.py
@@ -1,9 +1,11 @@
 """Fixtures to be shared across several tests."""
 
+import plotly.graph_objects as go
 import pytest
 
 import vizro.models as vm
 import vizro.plotly.express as px
+from vizro.tables import dash_data_table
 
 
 @pytest.fixture
@@ -24,6 +26,16 @@ def standard_px_chart(gapminder):
     )
 
 
+@pytest.fixture
+def standard_dash_table(gapminder):
+    return dash_data_table(data_frame=gapminder)
+
+
+@pytest.fixture
+def standard_go_chart(gapminder):
+    return go.Figure(data=go.Scatter(x=gapminder["gdpPercap"], y=gapminder["lifeExp"], mode="markers"))
+
+
 @pytest.fixture()
 def page1():
     return vm.Page(title="Page 1", components=[vm.Button(), vm.Button()])
diff --git a/vizro-core/tests/unit/vizro/models/_components/test_graph.py b/vizro-core/tests/unit/vizro/models/_components/test_graph.py
index 616d6d4e9..794837a97 100644
--- a/vizro-core/tests/unit/vizro/models/_components/test_graph.py
+++ b/vizro-core/tests/unit/vizro/models/_components/test_graph.py
@@ -14,11 +14,6 @@
 from vizro.models._components.graph import create_empty_fig
 
 
-@pytest.fixture
-def standard_go_chart(gapminder):
-    return go.Figure(data=go.Scatter(x=gapminder["gdpPercap"], y=gapminder["lifeExp"], mode="markers"))
-
-
 @pytest.fixture
 def standard_px_chart_with_str_dataframe():
     return px.scatter(
diff --git a/vizro-core/tests/unit/vizro/models/_components/test_table.py b/vizro-core/tests/unit/vizro/models/_components/test_table.py
new file mode 100644
index 000000000..5ef3d9a83
--- /dev/null
+++ b/vizro-core/tests/unit/vizro/models/_components/test_table.py
@@ -0,0 +1,123 @@
+"""Unit tests for vizro.models.Table."""
+import json
+
+import plotly
+import pytest
+from dash import dash_table, dcc, html
+from pydantic import ValidationError
+
+import vizro.models as vm
+import vizro.plotly.express as px
+from vizro.managers import data_manager
+from vizro.models._action._action import Action
+from vizro.tables import dash_data_table
+
+
+@pytest.fixture
+def dash_table_with_arguments():
+    return dash_data_table(data_frame=px.data.gapminder(), style_header={"border": "1px solid green"})
+
+
+@pytest.fixture
+def dash_table_with_str_dataframe():
+    return dash_data_table(data_frame="gapminder")
+
+
+@pytest.fixture
+def expected_table():
+    return dcc.Loading(
+        html.Div(
+            [
+                None,
+                html.Div(dash_table.DataTable(), id="text_table"),
+            ],
+            className="table-container",
+            id="text_table_outer",
+        )
+    )
+
+
+class TestDunderMethodsTable:
+    def test_create_graph_mandatory_only(self, standard_dash_table):
+        table = vm.Table(figure=standard_dash_table)
+
+        assert hasattr(table, "id")
+        assert table.type == "table"
+        assert table.figure == standard_dash_table
+        assert table.actions == []
+
+    @pytest.mark.parametrize("id", ["id_1", "id_2"])
+    def test_create_table_mandatory_and_optional(self, standard_dash_table, id):
+        table = vm.Table(
+            figure=standard_dash_table,
+            id=id,
+            actions=[],
+        )
+
+        assert table.id == id
+        assert table.type == "table"
+        assert table.figure == standard_dash_table
+
+    def test_mandatory_figure_missing(self):
+        with pytest.raises(ValidationError, match="field required"):
+            vm.Table()
+
+    def test_failed_table_with_no_captured_callable(self, standard_go_chart):
+        with pytest.raises(ValidationError, match="must provide a valid CapturedCallable object"):
+            vm.Table(
+                figure=standard_go_chart,
+            )
+
+    @pytest.mark.xfail(reason="This test is failing as we are not yet detecting different types of captured callables")
+    def test_failed_table_with_wrong_captured_callable(self, standard_px_chart):
+        with pytest.raises(ValidationError, match="must provide a valid table function vm.Table"):
+            vm.Table(
+                figure=standard_px_chart,
+            )
+
+    def test_getitem_known_args(self, dash_table_with_arguments):
+        table = vm.Table(figure=dash_table_with_arguments)
+        assert table["style_header"] == {"border": "1px solid green"}
+        assert table["type"] == "table"
+
+    def test_getitem_unknown_args(self, standard_dash_table):
+        table = vm.Table(figure=standard_dash_table)
+        with pytest.raises(KeyError):
+            table["unknown_args"]
+
+    def test_set_action_via_validator(self, standard_dash_table, test_action_function):
+        table = vm.Table(figure=standard_dash_table, actions=[Action(function=test_action_function)])
+        actions_chain = table.actions[0]
+        assert actions_chain.trigger.component_property == "active_cell"
+
+
+class TestProcessTableDataFrame:
+    def test_process_figure_data_frame_str_df(self, dash_table_with_str_dataframe, gapminder):
+        data_manager["gapminder"] = gapminder
+        table_with_str_df = vm.Table(
+            id="table",
+            figure=dash_table_with_str_dataframe,
+        )
+        assert data_manager._get_component_data("table").equals(gapminder)
+        assert table_with_str_df["data_frame"] == "gapminder"
+
+    def test_process_figure_data_frame_df(self, standard_dash_table, gapminder):
+        table_with_str_df = vm.Table(
+            id="table",
+            figure=standard_dash_table,
+        )
+        assert data_manager._get_component_data("table").equals(gapminder)
+        with pytest.raises(KeyError, match="'data_frame'"):
+            table_with_str_df.figure["data_frame"]
+
+
+class TestBuildTable:
+    def test_table_build(self, standard_dash_table, expected_table):
+        table = vm.Table(
+            id="text_table",
+            figure=standard_dash_table,
+        )
+
+        result = json.loads(json.dumps(table.build(), cls=plotly.utils.PlotlyJSONEncoder))
+        expected = json.loads(json.dumps(expected_table, cls=plotly.utils.PlotlyJSONEncoder))
+        assert result == expected
diff --git a/vizro-core/tests/unit/vizro/models/test_page.py b/vizro-core/tests/unit/vizro/models/test_page.py
index 78ef54520..0ccac6ac0 100644
--- a/vizro-core/tests/unit/vizro/models/test_page.py
+++ b/vizro-core/tests/unit/vizro/models/test_page.py
@@ -90,10 +90,15 @@ def test_set_layout_invalid(self):
         with pytest.raises(ValidationError, match="Number of page and grid components need to be the same."):
             vm.Page(title="Page 4", components=[vm.Button()], layout=vm.Layout(grid=[[0, 1]]))
 
-    def test_valid_component_types(self, standard_px_chart):
+    def test_valid_component_types(self, standard_px_chart, standard_dash_table):
         vm.Page(
             title="Page Title",
-            components=[vm.Graph(figure=standard_px_chart), vm.Card(text="""# Header 1"""), vm.Button()],
+            components=[
+                vm.Graph(figure=standard_px_chart),
+                vm.Card(text="""# Header 1"""),
+                vm.Button(),
+                vm.Table(figure=standard_dash_table),
+            ],
         )
 
     @pytest.mark.parametrize(
@@ -101,7 +106,7 @@ def test_valid_component_types(self, standard_px_chart):
         [vm.Checklist(), vm.Dropdown(), vm.RadioItems(), vm.RangeSlider(), vm.Slider()],
     )
     def test_invalid_component_types(self, test_component):
-        with pytest.raises(ValidationError, match=re.escape("(allowed values: 'button', 'card', 'graph')")):
+        with pytest.raises(ValidationError, match=re.escape("(allowed values: 'button', 'card', 'graph', 'table')")):
             vm.Page(title="Page Title", components=[test_component])
 
     def test_valid_control_types(self, standard_px_chart):
diff --git a/vizro-core/tests/unit/vizro/tables/test_dash_table.py b/vizro-core/tests/unit/vizro/tables/test_dash_table.py
new file mode 100644
index 000000000..722f89a6b
--- /dev/null
+++ b/vizro-core/tests/unit/vizro/tables/test_dash_table.py
@@ -0,0 +1,22 @@
+import pytest
+
+from vizro.tables.dash_table import _set_defaults_nested
+
+
+@pytest.fixture
+def default_dictionary():
+    return {"a": {"b": {"c": 1, "d": 2}}, "e": 3}
+
+
+@pytest.mark.parametrize(
+    "input, expected",
+    [
+        ({}, {"a": {"b": {"c": 1, "d": 2}}, "e": 3}),  # nothing supplied
+        ({"e": 10}, {"a": {"b": {"c": 1, "d": 2}}, "e": 10}),  # flat main key
+        ({"a": {"b": {"c": 11, "d": 12}}}, {"a": {"b": {"c": 11, "d": 12}}, "e": 3}),  # updated multiple nested keys
+        ({"a": {"b": {"c": 1, "d": {"f": 42}}}}, {"a": {"b": {"c": 1, "d": {"f": 42}}}, "e": 3}),  # add new dict
+        ({"a": {"b": {"c": 5}}}, {"a": {"b": {"c": 5, "d": 2}}, "e": 3}),  # arbitrary nesting
+    ],
+)
+def test_set_defaults_nested(default_dictionary, input, expected):
+    assert _set_defaults_nested(input, default_dictionary) == expected