Repository: deephaven/deephaven-plugins (language: python)

Issue:
ui.table Formatting
Needs design spec


Discussion / hints:
From https://github.com/deephaven/deephaven-core/issues/5817

<!-- Please read our Code of Conduct: https://github.com/deephaven/deephaven-core/blob/main/CODE_OF_CONDUCT.md -->
<!-- Please search existing issues to avoid creating duplicates. -->

<!-- Describe the feature you'd like. -->
As a user, I want to be able to call formatRowWhere more than once/have a method to pass in more than one condition/color pair so that I can easily color my rows using different conditions. For example:

`from deephaven import empty_table
t = empty_table(10).update(["X = i", "A = `A`"])
t2 = t.format_row_where("X % 2 == 0", "BLUE")
t3 = t2.format_row_where("X % 2 == 1", "GREEN")`

Wanted result: a table where rows alternate between blue and green.
Current result: a table where every other row is green, and the other rows are not formatted.

Possible bonus: apply the formats incrementally, with later calls taking precedent, e.g.

`from deephaven import empty_table
t = empty_table(10).update(["X = i", "A = `A`"])
t2 = t.format_row_where("X % 3 == 0", "BLUE")
t3 = t2.format_row_where("X % 2 == 0", "GREEN")`

Would result in some blue and green rows, with green overriding the blue when the condition is true for both.

Fix patch:
diff --git a/package-lock.json b/package-lock.json
index 322705379..43dda2e20 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11351,6 +11351,12 @@
         "@types/unist": "^2"
       }
     },
+    "node_modules/@types/memoizee": {
+      "version": "0.4.11",
+      "resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.11.tgz",
+      "integrity": "sha512-2gyorIBZu8GoDr9pYjROkxWWcFtHCquF7TVbN2I+/OvgZhnIGQS0vX5KJz4lXNKb8XOSfxFOSG5OLru1ESqLUg==",
+      "dev": true
+    },
     "node_modules/@types/minimatch": {
       "version": "3.0.5",
       "dev": true,
@@ -32209,6 +32215,7 @@
         "react-redux": "^7.x"
       },
       "devDependencies": {
+        "@types/memoizee": "^0.4.5",
         "@types/react": "^17.0.2",
         "react": "^17.0.2",
         "react-dom": "^17.0.2",
@@ -35843,6 +35850,7 @@
         "@deephaven/utils": "^0.95.0",
         "@fortawesome/react-fontawesome": "^0.2.0",
         "@internationalized/date": "^3.5.5",
+        "@types/memoizee": "^0.4.5",
         "@types/react": "^17.0.2",
         "classnames": "^2.5.1",
         "json-rpc-2.0": "^1.6.0",
@@ -41736,6 +41744,12 @@
         "@types/unist": "^2"
       }
     },
+    "@types/memoizee": {
+      "version": "0.4.11",
+      "resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.11.tgz",
+      "integrity": "sha512-2gyorIBZu8GoDr9pYjROkxWWcFtHCquF7TVbN2I+/OvgZhnIGQS0vX5KJz4lXNKb8XOSfxFOSG5OLru1ESqLUg==",
+      "dev": true
+    },
     "@types/minimatch": {
       "version": "3.0.5",
       "dev": true
diff --git a/plugins/ui/conf.py b/plugins/ui/conf.py
index e20cfdb4b..e111513dc 100644
--- a/plugins/ui/conf.py
+++ b/plugins/ui/conf.py
@@ -39,6 +39,8 @@
 # options for sphinx_autodoc_typehints
 always_use_bars_union = True
 
+strip_signature_backslash = True
+
 from deephaven_server import Server
 
 # need a server instance to pull types from the autodocs
diff --git a/plugins/ui/docs/components/table.md b/plugins/ui/docs/components/table.md
index 42a298b99..1d80c95e2 100644
--- a/plugins/ui/docs/components/table.md
+++ b/plugins/ui/docs/components/table.md
@@ -11,12 +11,143 @@ _t = empty_table(10).update("X=i")
 t = ui.table(_t)
 ```
 
-## UI Recommendations
+## UI recommendations
 
 1. It is not necessary to use a UI table if you do not need any of its properties. You can just use the Deephaven table directly.
 2. Use a UI table to show properties like filters as if the user had created them in the UI. Users can change the default values provided by the UI table, such as filters.
 3. UI tables handle ticking tables automatically, so you can pass any Deephaven table to a UI table.
 
+## Formatting
+
+You can format the table using the `format_` prop. This prop takes a `ui.TableFormmat` object or list of `ui.TableFormat` objects. `ui.TableFormat` is a dataclass that encapsulates the formatting options for a table. The full list of formatting options can be found in the [API Reference](#tableformat).
+
+### Formatting rows and columns
+
+Every formatting rule may optionally specify `cols` and `if_` properties. The `cols` property is a column name or list of column names to which the formatting rule applies. If `cols` is omitted, then the rule will be applied to the entire row. The `if_` property is a Deephaven formula that indicates a formatting rule should be applied conditionally. The `if_` property _must_ evaluate to a boolean. If `if_` is omitted, then the rule will be applied to every row. These may be combined to apply formatting to specific columns only when a condition is met.
+
+> [!NOTE]
+> The `if_` property is a Deephaven formula evaluated in the engine. You can think of it like adding a new boolean column using [`update_view`](https://deephaven.io/core/docs/reference/table-operations/select/update-view/).
+
+The following example shows how to format the `Sym` and `Exchange` columns with a red background and white text when the `Sym` is `DOG`.
+
+```python
+from deephaven import ui
+import deephaven.plot.express as dx
+
+t = ui.table(
+    dx.data.stocks(),
+    format_=[
+        ui.TableFormat(
+            cols=["Sym", "Exchange"],
+            if_="Sym = `DOG`",
+            background_color="red",
+            color="white",
+        )
+    ],
+)
+```
+
+### Formatting rule priority
+
+The last matching formatting rule for each property will be applied. This means the lowest priority rules should be first in the list with higher priority rules at the end.
+
+In the following example, the `Sym` column will have a red background with white text, and the rest of the table will have a blue background with white text.
+
+```python
+from deephaven import ui
+import deephaven.plot.express as dx
+
+t = ui.table(
+    dx.data.stocks(),
+    format_=[
+        ui.TableFormat(background_color="blue", color="white"),
+        ui.TableFormat(cols="Sym", background_color="red"),
+    ],
+)
+```
+
+### Formatting color
+
+Formatting rules for colors support Deephaven theme colors, hex colors, or any valid CSS color (e.g., `red`, `#ff0000`, `rgb(255, 0, 0)`). It is **recommended to use Deephaven theme colors** when possible to maintain a consistent look and feel across the UI. Theme colors will also automatically update if the user changes the theme.
+
+#### Formatting text color
+
+The `color` property sets the text color of the cell. If a cell has a `background_color`, but no `color` set, the text color will be set to black or white depending on which contrasts better with the background color. Setting the `color` property will override this behavior.
+
+The following example will make all text the foreground color except the `Sym` column, which will be white. In dark mode, the foreground color is white, and in light mode, it is black. In light mode, the `Sym` column will be nearly invisible because it is not a theme color.
+
+```py
+from deephaven import ui
+import deephaven.plot.express as dx
+
+t = ui.table(
+    dx.data.stocks(),
+    format_=[
+        ui.TableFormat(color="fg"),
+        ui.TableFormat(cols="Sym", color="white"),
+    ],
+)
+```
+
+#### Formatting background color
+
+The `background_color` property sets the background color of the cell. Setting the `background_color` without setting `color` will result in the text color automatically being set to black or white based on the contrast with the `background_color`.
+
+The following example will make all the background color what is usually the foreground color. This means the table will have a white background with black text in dark theme and a black background with white text in light theme. The `Sym` column text will be the accent color in both themes.
+
+```py
+from deephaven import ui
+import deephaven.plot.express as dx
+
+t = ui.table(
+    dx.data.stocks(),
+    format_=[
+        ui.TableFormat(background_color="fg"),
+        ui.TableFormat(cols="Sym", color="accent"),
+    ],
+)
+```
+
+### Formatting numeric values
+
+> [!WARNING]
+> Datetime values are considered numeric. If you provide a default format for numeric values, it will also apply to datetime values. It is recommended to specify `cols` when applying value formats.
+
+Numeric values can be formatted using the `value` property. The `value` property is a string that follows [the GWT Java NumberFormat syntax](https://www.gwtproject.org/javadoc/latest/com/google/gwt/i18n/client/NumberFormat.html). If a numeric format is applied to a non-numeric column, it will be ignored.
+
+This example will format the `Price` and `Dollars` columns with the dollar sign, a comma separator for every 3 digits, 2 decimal places, and a minimum of 1 digit to the left of the decimal point. The `Random` column will be formatted with 3 decimal places and will drop the leading zero if the absolute value is less than 1.
+
+```py
+from deephaven import ui
+import deephaven.plot.express as dx
+
+t = ui.table(
+    dx.data.stocks(),
+    format_=[
+        ui.TableFormat(cols=["Price", "Dollars"], value="$#,##0.00"),
+        ui.TableFormat(cols="Random", value="#.000")
+    ],
+)
+```
+
+### Formatting datetime and timestamp values
+
+Datetime and timestamp values can be formatted using the `value` property. The `value` property is a string that follows [the GWT Java DateTimeFormat syntax](https://www.gwtproject.org/javadoc/latest/com/google/gwt/i18n/client/DateTimeFormat.html) with additional support for nanoseconds. You may provide up to 9 `S` characters after the decimal to represent partial seconds down to nanoseconds.
+
+The following example formats the `Timestamp` column to show the short date of the week, day of the month, short month name, full year, hours, minutes, seconds, and microseconds with the user selected timezone.
+
+```py
+from deephaven import ui
+import deephaven.plot.express as dx
+
+t = ui.table(
+    dx.data.stocks(),
+    format_=[
+        ui.TableFormat(cols="Timestamp", value="E, dd MMM yyyy HH:mm:ss.SSSSSS z"),
+    ],
+)
+```
+
 ## Events
 
 You can listen for different user events on a `ui.table`. There is both a `press` and `double_press` event for `row`, `cell`, and `column`. These events typically correspond to a click or double click on the table. The event payloads include table data related to the event. For `row` and `column` events, the corresponding data within the viewport will be sent to the event handler. The viewport is typically the visible area &plusmn; a window equal to the visible area (e.g., if rows 5-10 are visible, rows 0-15 will be in the viewport).
@@ -42,7 +173,7 @@ t = ui.table(
 )
 ```
 
-## Context Menu
+## Context menu
 
 Items can be added to the bottom of the `ui.table` context menu (right-click menu) by using the `context_menu` or `context_header_menu` props. The `context_menu` prop adds items to the cell context menu, while the `context_header_menu` prop adds items to the column header context menu. You can pass either a single dictionary for a single item or a list of dictionaries for multiple items.
 
@@ -106,7 +237,7 @@ t = ui.table(
 )
 ```
 
-### Dynamic Menu Items
+### Dynamic menu items
 
 Menu items can be dynamically created by passing a function as the context item. The function will be called with the data of the cell that was clicked when the menu was opened, and must return the menu items or None if you do not want to add context menu items based on the cell info.
 
@@ -130,7 +261,7 @@ t = ui.table(
 )
 ```
 
-## Column Order and Visibility
+## Column order and visibility
 
 You can freeze columns to the front of the table using the `frozen_columns` prop. Frozen columns will always be visible on the left side of the table, even when the user scrolls horizontally. The `frozen_columns` prop takes a list of column names to freeze.
 
@@ -153,7 +284,7 @@ t = ui.table(
 
 ![Example of column order and visibility](../_assets/table_column_order.png)
 
-## Grouping Columns
+## Grouping columns
 
 Columns can be grouped visually using the `column_groups` prop. Columns in a column group are moved so they are next to each other, and a header spanning all columns in the group is added. Columns can be rearranged within a group, but they cannot be moved outside of the group without using the table sidebar menu.
 
@@ -195,7 +326,7 @@ t = ui.table(
 
 ![Example of column groups](../_assets/table_column_groups.png)
 
-## Always Fetching Some Columns
+## Always fetching some columns
 
 Deephaven only fetches data for visible rows and columns within a window around the viewport (typically the viewport plus 1 page in all directions). This reduces the amount of data transferred between the server and client and allows displaying tables with billions of rows. Sometimes you may need to always fetch columns, such as a key column for a row press event. You can use the `always_fetch_columns` prop to specify columns that should always be fetched regardless of their visibility.
 
@@ -218,7 +349,7 @@ t = ui.table(
 )
 ```
 
-## Quick Filters
+## Quick filters
 
 Quick filters are an easy way to filter the table while also showing the user what filters are currently applied. These filters are applied on the server via request from the client, so users may change the filters without affecting other users. Unlike a `where` statement to filter a table on the server, quick filters can be easily changed by the user.
 
@@ -265,6 +396,14 @@ t = ui.table(
 
 ## API Reference
 
+### Table
+
 ```{eval-rst}
 .. dhautofunction:: deephaven.ui.table
 ```
+
+### TableFormat
+
+```{eval-rst}
+.. dhautofunction:: deephaven.ui.TableFormat
+```
diff --git a/plugins/ui/src/deephaven/ui/components/__init__.py b/plugins/ui/src/deephaven/ui/components/__init__.py
index 049beb11e..1d35971a2 100644
--- a/plugins/ui/src/deephaven/ui/components/__init__.py
+++ b/plugins/ui/src/deephaven/ui/components/__init__.py
@@ -48,7 +48,7 @@
 from .tab_list import tab_list
 from .tab_panels import tab_panels
 from .tab import tab
-from .table import table
+from .table import table, TableDatabar, TableFormat
 from .tabs import tabs
 from .text import text
 from .text_area import text_area
@@ -108,6 +108,8 @@
     "stack",
     "switch",
     "table",
+    "TableDatabar",
+    "TableFormat",
     "tab_list",
     "tab_panels",
     "tabs",
diff --git a/plugins/ui/src/deephaven/ui/components/table.py b/plugins/ui/src/deephaven/ui/components/table.py
index 9f9558614..37f1a04a2 100644
--- a/plugins/ui/src/deephaven/ui/components/table.py
+++ b/plugins/ui/src/deephaven/ui/components/table.py
@@ -1,91 +1,100 @@
 from __future__ import annotations
-from typing import Literal
-
+from dataclasses import dataclass
+from typing import Literal, Any, Optional
+import logging
 from deephaven.table import Table
-from ..elements import UITable
+from ..elements import Element
 from .types import AlignSelf, DimensionValue, JustifySelf, LayoutFlex, Position
 from ..types import (
     CellPressCallback,
+    Color,
     ColumnGroup,
     ColumnName,
     ColumnPressCallback,
-    DatabarConfig,
     QuickFilterExpression,
     RowPressCallback,
     ResolvableContextMenuItem,
 )
+from .._internal import dict_to_react_props, RenderContext
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class TableFormat:
+    """
+    A formatting rule for a table.
+
+    Args:
+        cols: The columns to format. If None, the format will apply to the entire row.
+        if_: Deephaven expression to filter which rows should be formatted. Must resolve to a boolean.
+        color: The font color.
+        background_color: The cell background color.
+        alignment: The cell text alignment.
+        value: Format string for the cell value.
+            E.g. "0.00%" to format as a percentage with two decimal places.
+        mode: The cell rendering mode.
+            Currently only databar is supported as an alternate rendering mode.
+    Returns:
+        The TableFormat.
+    """
 
+    cols: ColumnName | list[ColumnName] | None = None
+    if_: str | None = None
+    color: Color | None = None
+    background_color: Color | None = None
+    alignment: Literal["left", "center", "right"] | None = None
+    value: str | None = None
+    mode: TableDatabar | None = None
 
-def table(
-    table: Table,
-    *,
-    on_row_press: RowPressCallback | None = None,
-    on_row_double_press: RowPressCallback | None = None,
-    on_cell_press: CellPressCallback | None = None,
-    on_cell_double_press: CellPressCallback | None = None,
-    on_column_press: ColumnPressCallback | None = None,
-    on_column_double_press: ColumnPressCallback | None = None,
-    always_fetch_columns: ColumnName | list[ColumnName] | bool | None = None,
-    quick_filters: dict[ColumnName, QuickFilterExpression] | None = None,
-    show_quick_filters: bool = False,
-    show_grouping_column: bool = True,
-    show_search: bool = False,
-    reverse: bool = False,
-    front_columns: list[ColumnName] | None = None,
-    back_columns: list[ColumnName] | None = None,
-    frozen_columns: list[ColumnName] | None = None,
-    hidden_columns: list[ColumnName] | None = None,
-    column_groups: list[ColumnGroup] | None = None,
-    density: Literal["compact", "regular", "spacious"] | None = None,
-    context_menu: (
-        ResolvableContextMenuItem | list[ResolvableContextMenuItem] | None
-    ) = None,
-    context_header_menu: (
-        ResolvableContextMenuItem | list[ResolvableContextMenuItem] | None
-    ) = None,
-    databars: list[DatabarConfig] | None = None,
-    key: str | None = None,
-    flex: LayoutFlex | None = None,
-    flex_grow: float | None = None,
-    flex_shrink: float | None = None,
-    flex_basis: DimensionValue | None = None,
-    align_self: AlignSelf | None = None,
-    justify_self: JustifySelf | None = None,
-    order: int | None = None,
-    grid_area: str | None = None,
-    grid_row: str | None = None,
-    grid_row_start: str | None = None,
-    grid_row_end: str | None = None,
-    grid_column: str | None = None,
-    grid_column_start: str | None = None,
-    grid_column_end: str | None = None,
-    margin: DimensionValue | None = None,
-    margin_top: DimensionValue | None = None,
-    margin_bottom: DimensionValue | None = None,
-    margin_start: DimensionValue | None = None,
-    margin_end: DimensionValue | None = None,
-    margin_x: DimensionValue | None = None,
-    margin_y: DimensionValue | None = None,
-    width: DimensionValue | None = None,
-    height: DimensionValue | None = None,
-    min_width: DimensionValue | None = None,
-    min_height: DimensionValue | None = None,
-    max_width: DimensionValue | None = None,
-    max_height: DimensionValue | None = None,
-    position: Position | None = None,
-    top: DimensionValue | None = None,
-    bottom: DimensionValue | None = None,
-    start: DimensionValue | None = None,
-    end: DimensionValue | None = None,
-    left: DimensionValue | None = None,
-    right: DimensionValue | None = None,
-    z_index: int | None = None,
-) -> UITable:
+
+@dataclass
+class TableDatabar:
+    """
+    A databar configuration for a table.
+
+    Args:
+        column: Name of the column to display as a databar.
+        value_column: Name of the column to use as the value for the databar.
+            If not provided, the databar will use the column value.
+
+            This can be useful if you want to display a databar with
+            a log scale, but display the actual value in the cell.
+            In this case, the value_column would be the log of the actual value.
+        min: Minimum value for the databar. Defaults to the minimum value in the column.
+
+            If a column name is provided, the minimum value will be the value in that column.
+            If a constant is providded, the minimum value will be that constant.
+        max: Maximum value for the databar. Defaults to the maximum value in the column.
+
+            If a column name is provided, the maximum value will be the value in that column.
+            If a constant is providded, the maximum value will be that constant.
+        axis: Whether the databar 0 value should be proportional to the min and max values,
+            in the middle of the cell, or on one side of the databar based on direction.
+        direction: The direction of the databar.
+        value_placement: Placement of the value relative to the databar.
+        color: The color of the databar.
+        opacity: The opacity of the databar.
+    """
+
+    column: ColumnName
+    value_column: ColumnName | None = None
+    min: ColumnName | float | None = None
+    max: ColumnName | float | None = None
+    axis: Literal["proportional", "middle", "directional"] | None = None
+    direction: Literal["LTR", "RTL"] | None = None
+    value_placement: Literal["beside", "overlap", "hide"] | None = None
+    color: Color | None = None
+    opacity: float | None = None
+
+
+class table(Element):
     """
     Customization to how a table is displayed, how it behaves, and listen to UI events.
 
     Args:
         table: The table to wrap
+        format_: A formatting rule or list of formatting rules for the table.
         on_row_press: The callback function to run when a row is clicked.
             The callback is invoked with the visible row data provided in a dictionary where the
             column names are the keys.
@@ -165,6 +174,92 @@ def table(
     Returns:
         The rendered Table.
     """
-    props = locals()
-    del props["table"]
-    return UITable(table, **props)
+
+    _props: dict[str, Any]
+    """
+    The props that are passed to the frontend
+    """
+
+    def __init__(
+        self,
+        table: Table,
+        *,
+        format_: TableFormat | list[TableFormat] | None = None,
+        on_row_press: RowPressCallback | None = None,
+        on_row_double_press: RowPressCallback | None = None,
+        on_cell_press: CellPressCallback | None = None,
+        on_cell_double_press: CellPressCallback | None = None,
+        on_column_press: ColumnPressCallback | None = None,
+        on_column_double_press: ColumnPressCallback | None = None,
+        always_fetch_columns: ColumnName | list[ColumnName] | bool | None = None,
+        quick_filters: dict[ColumnName, QuickFilterExpression] | None = None,
+        show_quick_filters: bool = False,
+        show_grouping_column: bool = True,
+        show_search: bool = False,
+        reverse: bool = False,
+        front_columns: list[ColumnName] | None = None,
+        back_columns: list[ColumnName] | None = None,
+        frozen_columns: list[ColumnName] | None = None,
+        hidden_columns: list[ColumnName] | None = None,
+        column_groups: list[ColumnGroup] | None = None,
+        density: Literal["compact", "regular", "spacious"] | None = None,
+        context_menu: (
+            ResolvableContextMenuItem | list[ResolvableContextMenuItem] | None
+        ) = None,
+        context_header_menu: (
+            ResolvableContextMenuItem | list[ResolvableContextMenuItem] | None
+        ) = None,
+        databars: list[TableDatabar] | None = None,
+        key: str | None = None,
+        flex: LayoutFlex | None = None,
+        flex_grow: float | None = None,
+        flex_shrink: float | None = None,
+        flex_basis: DimensionValue | None = None,
+        align_self: AlignSelf | None = None,
+        justify_self: JustifySelf | None = None,
+        order: int | None = None,
+        grid_area: str | None = None,
+        grid_row: str | None = None,
+        grid_row_start: str | None = None,
+        grid_row_end: str | None = None,
+        grid_column: str | None = None,
+        grid_column_start: str | None = None,
+        grid_column_end: str | None = None,
+        margin: DimensionValue | None = None,
+        margin_top: DimensionValue | None = None,
+        margin_bottom: DimensionValue | None = None,
+        margin_start: DimensionValue | None = None,
+        margin_end: DimensionValue | None = None,
+        margin_x: DimensionValue | None = None,
+        margin_y: DimensionValue | None = None,
+        width: DimensionValue | None = None,
+        height: DimensionValue | None = None,
+        min_width: DimensionValue | None = None,
+        min_height: DimensionValue | None = None,
+        max_width: DimensionValue | None = None,
+        max_height: DimensionValue | None = None,
+        position: Position | None = None,
+        top: DimensionValue | None = None,
+        bottom: DimensionValue | None = None,
+        start: DimensionValue | None = None,
+        end: DimensionValue | None = None,
+        left: DimensionValue | None = None,
+        right: DimensionValue | None = None,
+        z_index: int | None = None,
+    ) -> None:
+        props = locals()
+        del props["self"]
+        self._props = props
+        self._key = props.get("key")
+
+    @property
+    def name(self):
+        return "deephaven.ui.elements.UITable"
+
+    @property
+    def key(self) -> str | None:
+        return self._key
+
+    def render(self, context: RenderContext) -> dict[str, Any]:
+        logger.debug("Returning props %s", self._props)
+        return dict_to_react_props(self._props)
diff --git a/plugins/ui/src/deephaven/ui/elements/UITable.py b/plugins/ui/src/deephaven/ui/elements/UITable.py
deleted file mode 100644
index 09e228d60..000000000
--- a/plugins/ui/src/deephaven/ui/elements/UITable.py
+++ /dev/null
@@ -1,478 +0,0 @@
-from __future__ import annotations
-
-import logging
-import sys
-from typing import Callable, Literal, Sequence, Any, cast
-from warnings import warn
-
-if sys.version_info < (3, 11):
-    from typing_extensions import TypedDict, NotRequired
-else:
-    from typing import TypedDict, NotRequired
-
-from deephaven.table import Table
-from deephaven import SortDirection
-from .Element import Element
-from ..types import (
-    ColumnName,
-    AggregationOperation,
-    QuickFilterExpression,
-    Color,
-    CellPressCallback,
-    ColumnPressCallback,
-    DataBarAxis,
-    DataBarValuePlacement,
-    DataBarDirection,
-    SelectionMode,
-    TableSortDirection,
-    RowPressCallback,
-    StringSortDirection,
-)
-from .._internal import dict_to_react_props, RenderContext
-
-logger = logging.getLogger(__name__)
-
-
-def remap_sort_direction(direction: TableSortDirection | str) -> Literal["ASC", "DESC"]:
-    """
-    Remap the sort direction to the grid sort direction
-
-    Args:
-        direction: The deephaven sort direction or grid sort direction to remap
-
-    Returns:
-        The grid sort direction
-    """
-    if direction == SortDirection.ASCENDING:
-        return "ASC"
-    elif direction == SortDirection.DESCENDING:
-        return "DESC"
-    elif direction in {"ASC", "DESC"}:
-        return cast(StringSortDirection, direction)
-    raise ValueError(f"Invalid table sort direction: {direction}")
-
-
-class UITableProps(TypedDict):
-    on_row_press: NotRequired[RowPressCallback]
-    """
-    Callback function to run when a row is clicked.
-    The first parameter is the row index, and the second is the visible row data provided in a dictionary where the
-    column names are the keys.
-    """
-
-    on_row_double_press: NotRequired[RowPressCallback]
-    """
-    The callback function to run when a row is double clicked.
-    The first parameter is the row index, and the second is the visible row data provided in a dictionary where the
-    column names are the keys.
-    """
-
-    on_cell_press: NotRequired[CellPressCallback]
-    """
-    The callback function to run when a cell is clicked.
-    The first parameter is the cell index, and the second is the cell data provided in a dictionary where the
-    column names are the keys.
-    """
-
-    on_cell_double_press: NotRequired[CellPressCallback]
-    """
-    The callback function to run when a cell is double clicked.
-    The first parameter is the cell index, and the second is the cell data provided in a dictionary where the
-    column names are the keys.
-    """
-
-    on_column_press: NotRequired[ColumnPressCallback]
-    """
-    The callback function to run when a column is clicked.
-    The first parameter is the column name.
-    """
-
-    on_column_double_press: NotRequired[ColumnPressCallback]
-    """
-    The callback function to run when a column is double clicked.
-    The first parameter is the column name.
-    """
-
-    table: Table
-    """
-    The table to wrap
-    """
-
-
-class UITable(Element):
-    """
-    Wrap a Table with some extra props for giving hints to displaying a table
-    """
-
-    _props: UITableProps
-    """
-    The props that are passed to the frontend
-    """
-
-    def __init__(
-        self,
-        table: Table,
-        **props: Any,
-    ):
-        """
-        Create a UITable from the passed in table. UITable provides an [immutable fluent interface](https://en.wikipedia.org/wiki/Fluent_interface#Immutability) for adding UI hints to a table.
-
-        Args:
-            table: The table to wrap
-            props: UITableProps props to pass to the frontend.
-        """
-
-        # Store all the props that were passed in
-        self._props = UITableProps(**props, table=table)
-        self._key = props.get("key")
-
-    @property
-    def name(self):
-        return "deephaven.ui.elements.UITable"
-
-    @property
-    def key(self) -> str | None:
-        return self._key
-
-    def _with_prop(self, key: str, value: Any) -> "UITable":
-        """
-        Create a new UITable with the passed in prop added to the existing props
-
-        Args:
-            key: The key to add to the props
-            value: The value to add with the associated key
-
-        Returns:
-            A new UITable with the passed in prop added to the existing props
-        """
-        logger.debug("_with_prop(%s, %s)", key, value)
-        return UITable(**{**self._props, key: value})
-
-    def _with_appendable_prop(self, key: str, value: Any) -> "UITable":
-        """
-        Create a new UITable with the passed in prop added to the existing prop
-        list (if it exists) or a new list with the passed in value
-
-        Args:
-            key: The key to add to the props
-            value: The value to add with the associated key
-
-        Returns:
-            A new UITable with the passed in prop added to the existing props
-        """
-        logger.debug("_with_appendable_prop(%s, %s)", key, value)
-        existing = self._props.get(key, [])
-
-        if not isinstance(existing, list):
-            raise ValueError(f"Expected {key} to be a list")
-
-        value = value if isinstance(value, list) else [value]
-
-        return UITable(**{**self._props, key: existing + value})
-
-    def _with_dict_prop(self, key: str, value: dict[str, Any]) -> "UITable":
-        """
-        Create a new UITable with the passed in prop in a dictionary.
-        This will override any existing prop with the same key within
-        the dict stored at prop_name.
-
-
-        Args:
-            prop_name: The key to add to the props
-            value: The value to add with the associated key
-
-        Returns:
-            A new UITable with the passed in prop added to the existing props
-        """
-        logger.debug("_with_dict_prop(%s, %s)", key, value)
-        existing = (
-            self._props.get(key) or {}
-        )  # Turn missing or explicit None into empty dict
-        return UITable(**{**self._props, key: {**existing, **value}})  # type: ignore
-
-    def render(self, context: RenderContext) -> dict[str, Any]:
-        logger.debug("Returning props %s", self._props)
-        return dict_to_react_props({**self._props})
-
-    def aggregations(
-        self,
-        operations: dict[ColumnName, list[AggregationOperation]],
-        operation_order: list[AggregationOperation] | None = None,
-        default_operation: AggregationOperation | None = None,
-        group_by: list[ColumnName] | None = None,
-        show_on_top: bool = False,
-    ) -> "UITable":
-        """
-        Set the totals table to display below the main table.
-
-        Args:
-            operations: The operations to apply to the columns of the table.
-            operation_order: The order in which to display the operations.
-            default_operation: The default operation to apply to columns that do not have an operation specified.
-            group_by: The columns to group by.
-            show_on_top: Whether to show the totals table above the main table.
-
-        Returns:
-            A new UITable
-        """
-        raise NotImplementedError()
-
-    def always_fetch_columns(self, columns: str | list[str]) -> "UITable":
-        """
-        Set the columns to always fetch from the server.
-        These will not be affected by the users current viewport/horizontal scrolling.
-        Useful if you have a column with key value data that you want to always include
-        in the data sent for row click operations.
-
-        Args:
-            columns: The columns to always fetch from the server.
-                May be a single column name.
-
-        Returns:
-            A new UITable
-        """
-        return self._with_appendable_prop("always_fetch_columns", columns)
-
-    def back_columns(self, columns: str | list[str]) -> "UITable":
-        """
-        Set the columns to show at the back of the table.
-        These will not be moveable in the UI.
-
-        Args:
-            columns: The columns to show at the back of the table.
-                May be a single column name.
-
-        Returns:
-            A new UITable
-        """
-        raise NotImplementedError()
-
-    def column_group(
-        self, name: str, children: list[str], color: str | None
-    ) -> "UITable":
-        """
-        Create a group for columns in the table.
-
-        Args:
-            name: The group name. Must be a valid column name and not a duplicate of another column or group.
-            children: The children in the group. May contain column names or other group names.
-                Each item may only be specified as a child once.
-            color: The hex color string or Deephaven color name.
-
-        Returns:
-            A new UITable
-        """
-        raise NotImplementedError()
-
-    def color_column(
-        self,
-        column: ColumnName,
-        where: QuickFilterExpression | None = None,
-        color: Color | None = None,
-        background_color: Color | None = None,
-    ) -> "UITable":
-        """
-        Applies color formatting to a column of the table.
-
-        Args:
-            column: The column name
-            where: The filter to apply to the expression.
-              Uses quick filter format (e.g. `>10`).
-            color: The text color. Accepts hex color strings or Deephaven color names.
-            background_color: The background color. Accepts hex color strings or Deephaven color names.
-
-        Returns:
-            A new UITable
-        """
-        raise NotImplementedError()
-
-    def color_row(
-        self,
-        column: ColumnName,
-        where: QuickFilterExpression | None = None,
-        color: Color | None = None,
-        background_color: Color | None = None,
-    ) -> "UITable":
-        """
-        Applies color formatting to rows of the table conditionally based on the value of a column.
-
-        Args:
-            column: The column name
-            where: The filter to apply to the expression.
-              Uses quick filter format (e.g. `>10`).
-            color: The text color. Accepts hex color strings or Deephaven color names.
-            background_color: The background color. Accepts hex color strings or Deephaven color names.
-
-        Returns:
-            A new UITable
-        """
-        raise NotImplementedError()
-
-    def data_bar(
-        self,
-        col: str,
-        value_col: str | None = None,
-        min: float | str | None = None,
-        max: float | str | None = None,
-        axis: DataBarAxis | None = None,
-        positive_color: Color | list[Color] | None = None,
-        negative_color: Color | list[Color] | None = None,
-        value_placement: DataBarValuePlacement | None = None,
-        direction: DataBarDirection | None = None,
-        opacity: float | None = None,
-        marker_col: str | None = None,
-        marker_color: Color | None = None,
-    ) -> "UITable":
-        """
-        Applies data bar formatting to the specified column.
-
-        Args:
-            col: Column to generate data bars in
-            value_col: Column containing the values to generate data bars from
-            min: Minimum value for data bar scaling or column to get value from
-            max: Maximum value for data bar scaling or column to get value from
-            axis: Orientation of data bar relative to cell
-            positive_color: Color for positive bars. Use list of colors to form a gradient
-            negative_color: Color for negative bars. Use list of colors to form a gradient
-            value_placement: Orientation of values relative to data bar
-            direction: Orientation of data bar relative to horizontal axis
-            opacity: Opacity of data bar. Accepts values from 0 to 1
-            marker_col: Column containing the values to generate markers from
-            marker_color: Color for markers
-
-        Returns:
-            A new UITable
-        """
-        raise NotImplementedError()
-
-    def format(self, column: ColumnName, format: str) -> "UITable":
-        """
-        Specify the formatting to display a column in.
-
-        Args:
-            column: The column name
-            format: The format to display the column in. Valid format depends on column type
-
-        Returns:
-            A new UITable
-        """
-        raise NotImplementedError()
-
-    def freeze_columns(self, columns: str | list[str]) -> "UITable":
-        """
-        Set the columns to freeze to the front of the table.
-        These will always be visible and not affected by horizontal scrolling.
-
-        Args:
-            columns: The columns to freeze to the front of the table.
-
-        Returns:
-            A new UITable
-        """
-        raise NotImplementedError()
-
-    def front_columns(self, columns: str | list[str]) -> "UITable":
-        """
-        Set the columns to show at the front of the table. These will not be moveable in the UI.
-
-        Args:
-            columns: The columns to show at the front of the table.
-
-        Returns:
-            A new UITable
-        """
-        raise NotImplementedError()
-
-    def hide_columns(self, columns: str | list[str]) -> "UITable":
-        """
-        Set the columns to hide by default in the table. The user can still resize the columns to view them.
-
-        Args:
-            columns: The columns to hide from the table. May be a single column name.
-
-        Returns:
-            A new UITable
-        """
-        raise NotImplementedError()
-
-    def on_row_double_press(self, callback: RowPressCallback) -> "UITable":
-        """
-        Add a callback for when a row is double clicked.
-        *Deprecated: Use the on_row_double_press keyword arg instead.
-
-        Args:
-            callback: The callback function to run when a row is double clicked.
-              The first parameter is the row index, and the second is the row data provided in a dictionary where the
-              column names are the keys.
-
-        Returns:
-            A new UITable
-        """
-        warn(
-            "on_row_double_press function is deprecated. Use the on_row_double_press keyword arg instead.",
-            DeprecationWarning,
-            stacklevel=2,
-        )
-        return self._with_prop("on_row_double_press", callback)
-
-    def selection_mode(self, mode: SelectionMode) -> "UITable":
-        """
-        Set the selection mode for the table.
-
-        Args:
-            mode: The selection mode to use. Must be one of `"ROW"`, `"COLUMN"`, or `"CELL"`
-            `"ROW"` selects the entire row of the cell you click on.
-            `"COLUMN"` selects the entire column of the cell you click on.
-            `"CELL"` selects only the cells you click on.
-
-        Returns:
-            A new UITable
-        """
-        raise NotImplementedError()
-
-    def sort(
-        self,
-        by: str | Sequence[str],
-        direction: TableSortDirection | Sequence[TableSortDirection] | None = None,
-    ) -> "UITable":
-        """
-        Provide the default sort that will be used by the UI.
-
-        Args:
-            by: The column(s) to sort by. May be a single column name, or a list of column names.
-            direction: The sort direction(s) to use. If provided, that must match up with the columns provided.
-                May be a single sort direction, or a list of sort directions. The possible sort directions are
-                `"ASC"` `"DESC"`, `SortDirection.ASCENDING`, and `SortDirection.DESCENDING`.
-                Defaults to "ASC".
-
-        Returns:
-            A new UITable
-        """
-        direction_list: Sequence[TableSortDirection] = []
-        if direction:
-            direction_list_unmapped = (
-                direction if isinstance(direction, Sequence) else [direction]
-            )
-
-            # map deephaven sort direction to frontend sort direction
-            direction_list = [
-                remap_sort_direction(direction) for direction in direction_list_unmapped
-            ]
-
-        by_list = [by] if isinstance(by, str) else by
-
-        if direction and len(direction_list) != len(by_list):
-            raise ValueError("by and direction must be the same length")
-
-        if direction:
-            sorts = [
-                {"column": column, "direction": direction, "is_abs": False}
-                for column, direction in zip(by_list, direction_list)
-            ]
-        else:
-            sorts = [
-                {"column": column, "direction": "ASC", "is_abs": False}
-                for column in by_list
-            ]
-
-        return self._with_prop("sorts", sorts)
diff --git a/plugins/ui/src/deephaven/ui/elements/__init__.py b/plugins/ui/src/deephaven/ui/elements/__init__.py
index 01aa1c109..21143a2f9 100644
--- a/plugins/ui/src/deephaven/ui/elements/__init__.py
+++ b/plugins/ui/src/deephaven/ui/elements/__init__.py
@@ -2,7 +2,6 @@
 from .BaseElement import BaseElement
 from .DashboardElement import DashboardElement
 from .FunctionElement import FunctionElement
-from .UITable import UITable
 
 __all__ = [
     "BaseElement",
@@ -10,5 +9,4 @@
     "Element",
     "FunctionElement",
     "PropsType",
-    "UITable",
 ]
diff --git a/plugins/ui/src/deephaven/ui/renderer/Renderer.py b/plugins/ui/src/deephaven/ui/renderer/Renderer.py
index ab834aa9b..5f7161adb 100644
--- a/plugins/ui/src/deephaven/ui/renderer/Renderer.py
+++ b/plugins/ui/src/deephaven/ui/renderer/Renderer.py
@@ -2,7 +2,8 @@
 from dataclasses import asdict as dataclass_asdict, is_dataclass
 import logging
 from typing import Any, Union
-from .._internal import RenderContext
+
+from .._internal import RenderContext, remove_empty_keys
 from ..elements import Element, PropsType
 from .RenderedNode import RenderedNode
 
@@ -32,7 +33,8 @@ def _render_child_item(item: Any, parent_context: RenderContext, index_key: str)
     # If the item is an instance of a dataclass
     if is_dataclass(item) and not isinstance(item, type):
         return _render_dict(
-            dataclass_asdict(item), parent_context.get_child_context(index_key)
+            remove_empty_keys(dataclass_asdict(item)),
+            parent_context.get_child_context(index_key),
         )
 
     if isinstance(item, Element):
diff --git a/plugins/ui/src/deephaven/ui/types/types.py b/plugins/ui/src/deephaven/ui/types/types.py
index 6e1f2ccb1..7128d3b27 100644
--- a/plugins/ui/src/deephaven/ui/types/types.py
+++ b/plugins/ui/src/deephaven/ui/types/types.py
@@ -545,71 +545,3 @@ class DateRange(TypedDict):
     """
     End value for the date range.
     """
-
-
-DataBarAxis = Literal["PROPORTIONAL", "MIDDLE", "DIRECTIONAL"]
-DataBarDirection = Literal["LTR", "RTL"]
-DataBarValuePlacement = Literal["BESIDE", "OVERLAP", "HIDE"]
-
-
-class DatabarConfig(TypedDict):
-    """
-    Configuration for displaying a databar.
-    """
-
-    column: ColumnName
-    """
-    Name of the column to display as a databar.
-    """
-
-    value_column: NotRequired[ColumnName]
-    """
-    Name of the column to use as the value for the databar.
-    If not provided, the databar will use the column value.
-
-    This can be useful if you want to display a databar with
-    a log scale, but display the actual value in the cell.
-    In this case, the value_column would be the log of the actual value.
-    """
-
-    min: NotRequired[Union[ColumnName, float]]
-    """
-    Minimum value for the databar. Defaults to the minimum value in the column.
-
-    If a column name is provided, the minimum value will be the value in that column.
-    If a constant is providded, the minimum value will be that constant.
-    """
-
-    max: NotRequired[Union[ColumnName, float]]
-    """
-    Maximum value for the databar. Defaults to the maximum value in the column.
-
-    If a column name is provided, the maximum value will be the value in that column.
-    If a constant is providded, the maximum value will be that constant.
-    """
-
-    axis: NotRequired[DataBarAxis]
-    """
-    Whether the databar 0 value should be proportional to the min and max values,
-    in the middle of the cell, or on one side of the databar based on direction.
-    """
-
-    direction: NotRequired[DataBarDirection]
-    """
-    Direction of the databar.
-    """
-
-    value_placement: NotRequired[DataBarValuePlacement]
-    """
-    Placement of the value relative to the databar.
-    """
-
-    color: NotRequired[Color]
-    """
-    Color of the databar.
-    """
-
-    opacity: NotRequired[float]
-    """
-    Opacity of the databar fill.
-    """
diff --git a/plugins/ui/src/js/package.json b/plugins/ui/src/js/package.json
index b6983ac88..fc0cb8581 100644
--- a/plugins/ui/src/js/package.json
+++ b/plugins/ui/src/js/package.json
@@ -29,6 +29,7 @@
     "update-dh-packages": "node ../../../../tools/update-dh-packages.mjs"
   },
   "devDependencies": {
+    "@types/memoizee": "^0.4.5",
     "@types/react": "^17.0.2",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
diff --git a/plugins/ui/src/js/src/elements/UITable/JsTableProxy.ts b/plugins/ui/src/js/src/elements/UITable/JsTableProxy.ts
index 8bc2c3126..ebae4cd5e 100644
--- a/plugins/ui/src/js/src/elements/UITable/JsTableProxy.ts
+++ b/plugins/ui/src/js/src/elements/UITable/JsTableProxy.ts
@@ -21,7 +21,11 @@ interface JsTableProxy extends dh.Table {}
 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
 // @ts-ignore
 class JsTableProxy implements dh.Table {
-  static HIDDEN_COLUMN_SUFFIXES = ['__DATABAR_Min', '__DATABAR_Max'];
+  static HIDDEN_COLUMN_SUFFIXES = [
+    '__DATABAR_Min',
+    '__DATABAR_Max',
+    '__FORMAT',
+  ];
 
   private table: dh.Table;
 
diff --git a/plugins/ui/src/js/src/elements/UITable/UITable.tsx b/plugins/ui/src/js/src/elements/UITable/UITable.tsx
index 6ebb6ea9f..65e1f6834 100644
--- a/plugins/ui/src/js/src/elements/UITable/UITable.tsx
+++ b/plugins/ui/src/js/src/elements/UITable/UITable.tsx
@@ -45,6 +45,7 @@ function useStableArray<T>(array: T[]): T[] {
 }
 
 export function UITable({
+  format_: formatProp,
   onCellPress,
   onCellDoublePress,
   onColumnPress,
@@ -127,9 +128,13 @@ export function UITable({
     hiddenColumns,
     columnGroups,
   });
+
+  // TODO: #982 respond to prop changes here
+  const [format] = useState(formatProp != null ? ensureArray(formatProp) : []);
+  // TODO: #981 move databars to format and rewire for databar support
   const [databars] = useState(databarsProp ?? []);
 
-  const databarColorMap = useMemo(() => {
+  const colorMap = useMemo(() => {
     log.debug('Theme changed, updating databar color map', theme);
     const colorSet = new Set<string>();
     databars?.forEach(databar => {
@@ -155,21 +160,31 @@ export function UITable({
       });
     });
 
+    format.forEach(rule => {
+      const { color, background_color: backgroundColor } = rule;
+      if (color != null) {
+        colorSet.add(color);
+      }
+      if (backgroundColor != null) {
+        colorSet.add(backgroundColor);
+      }
+    });
+
     const colorRecord: Record<string, string> = {};
     colorSet.forEach(c => {
       colorRecord[c] = colorValueStyle(c);
     });
 
     const resolvedColors = resolveCssVariablesInRecord(colorRecord);
-    const colorMap = new Map<string, string>();
+    const newColorMap = new Map<string, string>();
     Object.entries(resolvedColors).forEach(([key, value]) => {
-      colorMap.set(key, value);
+      newColorMap.set(key, value);
     });
-    return colorMap;
-  }, [databars, theme]);
+    return newColorMap;
+  }, [databars, format, theme]);
 
   if (model) {
-    model.setDatabarColorMap(databarColorMap);
+    model.setColorMap(colorMap);
   }
 
   const hydratedSorts = useMemo(() => {
@@ -214,7 +229,8 @@ export function UITable({
           dh,
           table,
           databars,
-          layoutHints
+          layoutHints,
+          format
         );
         if (!isCancelled) {
           setError(null);
@@ -235,7 +251,7 @@ export function UITable({
     return () => {
       isCancelled = true;
     };
-  }, [databars, dh, exportedTable, layoutHints]);
+  }, [databars, dh, exportedTable, layoutHints, format]);
 
   const modelColumns = model?.columns ?? EMPTY_ARRAY;
 
diff --git a/plugins/ui/src/js/src/elements/UITable/UITableModel.ts b/plugins/ui/src/js/src/elements/UITable/UITableModel.ts
index 1c5e2af5b..4fc494f47 100644
--- a/plugins/ui/src/js/src/elements/UITable/UITableModel.ts
+++ b/plugins/ui/src/js/src/elements/UITable/UITableModel.ts
@@ -3,24 +3,31 @@ import {
   DataBarOptions,
   CellRenderType,
   ModelIndex,
+  GridColor,
+  NullableGridColor,
+  memoizeClear,
+  GridRenderer,
 } from '@deephaven/grid';
 import {
   ColumnName,
   IrisGridModel,
   IrisGridModelFactory,
+  type IrisGridThemeType,
   isIrisGridTableModelTemplate,
   UIRow,
 } from '@deephaven/iris-grid';
+import { ensureArray } from '@deephaven/utils';
 import { TableUtils } from '@deephaven/jsapi-utils';
 import { type dh as DhType } from '@deephaven/jsapi-types';
-import { ColorGradient, DatabarConfig } from './UITableUtils';
+import { ColorGradient, DatabarConfig, FormattingRule } from './UITableUtils';
 import JsTableProxy, { UITableLayoutHints } from './JsTableProxy';
 
 export async function makeUiTableModel(
   dh: typeof DhType,
   table: DhType.Table,
   databars: DatabarConfig[],
-  layoutHints: UITableLayoutHints
+  layoutHints: UITableLayoutHints,
+  format: FormattingRule[]
 ): Promise<UITableModel> {
   const joinColumns: string[] = [];
   const totalsOperationMap: Record<string, string[]> = {};
@@ -56,6 +63,31 @@ export async function makeUiTableModel(
 
   let baseTable = table;
 
+  const customColumns: string[] = [];
+  format.forEach((rule, i) => {
+    const { if_ } = rule;
+    if (if_ != null) {
+      customColumns.push(`${getFormatCustomColumnName(i)}=${if_}`);
+    }
+  });
+
+  if (customColumns.length > 0) {
+    await new TableUtils(dh).applyCustomColumns(baseTable, customColumns);
+    format.forEach((rule, i) => {
+      const { if_ } = rule;
+      if (if_ != null) {
+        const columnType = baseTable.findColumn(
+          getFormatCustomColumnName(i)
+        ).type;
+        if (!TableUtils.isBooleanType(columnType)) {
+          throw new Error(
+            `ui.TableFormat if_ must be a boolean column. "${if_}" is a ${columnType} column`
+          );
+        }
+      }
+    });
+  }
+
   if (joinColumns.length > 0) {
     const totalsTable = await table.getTotalsTable({
       operationMap: totalsOperationMap,
@@ -80,9 +112,19 @@ export async function makeUiTableModel(
     model: baseModel,
     table: uiTableProxy,
     databars,
+    format,
   });
 }
 
+/**
+ * Gets the name of the custom column that stores the where clause for a formatting rule
+ * @param i The index of the formatting rule
+ * @returns The name of the custom column that stores the where clause for the formatting rule
+ */
+function getFormatCustomColumnName(i: number): string {
+  return `_${i}__FORMAT`;
+}
+
 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
 // @ts-ignore
 // eslint-disable-next-line @typescript-eslint/no-empty-interface
@@ -103,18 +145,25 @@ class UITableModel extends IrisGridModel {
 
   private databars: Map<ColumnName, DatabarConfig>;
 
-  private databarColorMap: Map<string, string> = new Map();
+  /**
+   * Map of theme color keys to hex color values
+   */
+  private colorMap: Map<string, string> = new Map();
+
+  private format: FormattingRule[];
 
   constructor({
     dh,
     model,
     table,
     databars,
+    format,
   }: {
     dh: typeof DhType;
     model: IrisGridModel;
     table: DhType.Table;
     databars: DatabarConfig[];
+    format: FormattingRule[];
   }) {
     super(dh);
 
@@ -126,6 +175,8 @@ class UITableModel extends IrisGridModel {
       this.databars.set(databar.column, databar);
     });
 
+    this.format = format;
+
     // eslint-disable-next-line no-constructor-return
     return new Proxy(this, {
       // We want to use any properties on the proxy model if defined
@@ -169,8 +220,8 @@ class UITableModel extends IrisGridModel {
     });
   }
 
-  setDatabarColorMap(colorMap: Map<string, string>): void {
-    this.databarColorMap = colorMap;
+  setColorMap(colorMap: Map<string, string>): void {
+    this.colorMap = colorMap;
   }
 
   // eslint-disable-next-line class-methods-use-this
@@ -285,7 +336,7 @@ class UITableModel extends IrisGridModel {
           typeof markerValue === 'string'
             ? this.getDatabarValueFromRow(row, markerValue, 'marker')
             : markerValue,
-        color: this.databarColorMap.get(markerColor) ?? markerColor,
+        color: this.colorMap.get(markerColor) ?? markerColor,
       };
     });
 
@@ -304,18 +355,18 @@ class UITableModel extends IrisGridModel {
 
     if (Array.isArray(positiveColor)) {
       positiveColor = positiveColor.map(
-        color => this.databarColorMap.get(color) ?? color
+        color => this.colorMap.get(color) ?? color
       );
     } else {
-      positiveColor = this.databarColorMap.get(positiveColor) ?? positiveColor;
+      positiveColor = this.colorMap.get(positiveColor) ?? positiveColor;
     }
 
     if (Array.isArray(negativeColor)) {
       negativeColor = negativeColor.map(
-        color => this.databarColorMap.get(color) ?? color
+        color => this.colorMap.get(color) ?? color
       );
     } else {
-      negativeColor = this.databarColorMap.get(negativeColor) ?? negativeColor;
+      negativeColor = this.colorMap.get(negativeColor) ?? negativeColor;
     }
 
     return {
@@ -330,6 +381,139 @@ class UITableModel extends IrisGridModel {
       value,
     };
   }
+
+  formatColumnMatch = memoizeClear(
+    (columns: string[], column: string): boolean =>
+      columns.some(c => c === column),
+    { primitive: true, max: 10000 }
+  );
+
+  /**
+   * Gets the first matching format option for a cell.
+   * Checks if there is a column match and if there is a where clause match if needed.
+   * If there is no value for the key that matches in any rule, returns undefined.
+   * Stops on first match.
+   *
+   * @param column The model column index
+   * @param row The model row index
+   * @param formatKey The key to get the format option for
+   * @returns The format option if set or undefined
+   */
+  getFormatOptionForCell<K extends keyof FormattingRule>(
+    column: ModelIndex,
+    row: ModelIndex,
+    formatKey: K
+  ): FormattingRule[K] | undefined {
+    if (!isIrisGridTableModelTemplate(this.model)) {
+      return undefined;
+    }
+    const columnName = this.columns[column].name;
+
+    // Iterate in reverse so that the last rule that matches is used
+    for (let i = this.format.length - 1; i >= 0; i -= 1) {
+      const rule = this.format[i];
+      const { cols, if_, [formatKey]: formatValue } = rule;
+      if (formatValue == null) {
+        // eslint-disable-next-line no-continue
+        continue;
+      }
+      if (
+        cols == null ||
+        this.formatColumnMatch(ensureArray(cols), columnName)
+      ) {
+        if (if_ == null) {
+          return formatValue;
+        }
+        const rowValues = this.model.row(row)?.data;
+        if (rowValues == null) {
+          return undefined;
+        }
+        const whereValue = rowValues.get(getFormatCustomColumnName(i))?.value;
+        if (whereValue === true) {
+          return formatValue;
+        }
+      }
+    }
+    return undefined;
+  }
+
+  getCachedFormatForCell = memoizeClear(
+    (
+      format: DhType.Format | undefined,
+      formatString: string | null | undefined
+    ): DhType.Format | undefined => ({
+      ...format,
+      formatString,
+    }),
+    { max: 10000 }
+  );
+
+  override formatForCell(
+    column: ModelIndex,
+    row: ModelIndex
+  ): DhType.Format | undefined {
+    const format = this.model.formatForCell(column, row);
+    return this.getCachedFormatForCell(
+      format,
+      this.getFormatOptionForCell(column, row, 'value') ?? format?.formatString
+    );
+  }
+
+  override colorForCell(
+    column: ModelIndex,
+    row: ModelIndex,
+    theme: IrisGridThemeType
+  ): GridColor {
+    const color = this.getFormatOptionForCell(column, row, 'color');
+    const { colorMap } = this;
+
+    // If a color is explicitly set, use it
+    if (color != null) {
+      return colorMap.get(color) ?? color;
+    }
+
+    // If there is a background color, use white or black depending on the background color
+    const backgroundColor = this.getFormatOptionForCell(
+      column,
+      row,
+      'background_color'
+    );
+
+    if (backgroundColor != null) {
+      const isDarkBackground = GridRenderer.getCachedColorIsDark(
+        colorMap.get(backgroundColor) ?? backgroundColor
+      );
+      return isDarkBackground ? theme.white : theme.black;
+    }
+
+    return this.model.colorForCell(column, row, theme);
+  }
+
+  override textAlignForCell(
+    column: ModelIndex,
+    row: ModelIndex
+  ): CanvasTextAlign {
+    return (
+      this.getFormatOptionForCell(column, row, 'alignment') ??
+      this.model.textAlignForCell(column, row)
+    );
+  }
+
+  override backgroundColorForCell(
+    column: ModelIndex,
+    row: ModelIndex,
+    theme: IrisGridThemeType
+  ): NullableGridColor {
+    const backgroundColor = this.getFormatOptionForCell(
+      column,
+      row,
+      'background_color'
+    );
+    if (backgroundColor != null) {
+      return this.colorMap.get(backgroundColor) ?? backgroundColor;
+    }
+    return this.model.backgroundColorForCell(column, row, theme);
+  }
 }
 
 export default UITableModel;
diff --git a/plugins/ui/src/js/src/elements/UITable/UITableUtils.tsx b/plugins/ui/src/js/src/elements/UITable/UITableUtils.tsx
index bbaa7f8cc..597ec0cd5 100644
--- a/plugins/ui/src/js/src/elements/UITable/UITableUtils.tsx
+++ b/plugins/ui/src/js/src/elements/UITable/UITableUtils.tsx
@@ -38,8 +38,19 @@ export type DatabarConfig = {
   markers?: { value: number | string; color?: string }[];
 };
 
+export type FormattingRule = {
+  cols?: ColumnName | ColumnName[];
+  if_?: string;
+  color?: string;
+  background_color?: string;
+  alignment?: 'left' | 'center' | 'right';
+  value?: string;
+  mode?: DatabarConfig;
+};
+
 export type UITableProps = StyleProps & {
   table: dh.WidgetExportedObject;
+  format_?: FormattingRule | FormattingRule[];
   onCellPress?: (data: CellData) => void;
   onCellDoublePress?: (data: CellData) => void;
   onRowPress?: (rowData: RowDataMap) => void;


Test patch:
diff --git a/plugins/ui/src/js/src/elements/UITable/UITableModel.test.ts b/plugins/ui/src/js/src/elements/UITable/UITableModel.test.ts
new file mode 100644
index 000000000..0c7df094e
--- /dev/null
+++ b/plugins/ui/src/js/src/elements/UITable/UITableModel.test.ts
@@ -0,0 +1,214 @@
+import {
+  IrisGridThemeType,
+  type IrisGridTableModel,
+} from '@deephaven/iris-grid';
+import { GridRenderer } from '@deephaven/grid';
+import { type dh } from '@deephaven/jsapi-types';
+import { TestUtils } from '@deephaven/test-utils';
+import UITableModel from './UITableModel';
+
+const MOCK_DH = TestUtils.createMockProxy<typeof dh>();
+
+const MOCK_BASE_MODEL = TestUtils.createMockProxy<IrisGridTableModel>({
+  columns: [
+    {
+      name: 'column0',
+      type: 'string',
+    },
+    {
+      name: 'column1',
+      type: 'int',
+    },
+  ] as dh.Column[],
+});
+
+const MOCK_TABLE = TestUtils.createMockProxy<dh.Table>();
+
+describe('Formatting', () => {
+  describe('getFormatOptionForCell', () => {
+    test('applies last rule for an option', () => {
+      const model = new UITableModel({
+        dh: MOCK_DH,
+        model: MOCK_BASE_MODEL,
+        table: MOCK_TABLE,
+        databars: [],
+        format: [{ color: 'red' }, { color: 'blue' }],
+      });
+      expect(model.getFormatOptionForCell(0, 0, 'color')).toBe('blue');
+      expect(model.getFormatOptionForCell(1, 1, 'color')).toBe('blue');
+    });
+
+    test('only applies rules matching the column', () => {
+      const model = new UITableModel({
+        dh: MOCK_DH,
+        model: MOCK_BASE_MODEL,
+        table: MOCK_TABLE,
+        databars: [],
+        format: [
+          { cols: 'column0', color: 'red' },
+          { cols: 'column1', color: 'blue' },
+        ],
+      });
+      expect(model.getFormatOptionForCell(0, 0, 'color')).toBe('red');
+      expect(model.getFormatOptionForCell(1, 1, 'color')).toBe('blue');
+    });
+
+    test('only applies rules matching the condition', () => {
+      (MOCK_BASE_MODEL.row as jest.Mock).mockImplementation(r => ({
+        data: {
+          get: () => ({
+            value: r % 2 === 0, // Even rows are true
+          }),
+        },
+      }));
+
+      const model = new UITableModel({
+        dh: MOCK_DH,
+        model: MOCK_BASE_MODEL,
+        table: MOCK_TABLE,
+        databars: [],
+        format: [
+          { color: 'red', if_: 'even' },
+          { cols: 'column1', color: 'blue', if_: 'even' },
+        ],
+      });
+      expect(model.getFormatOptionForCell(0, 0, 'color')).toBe('red');
+      expect(model.getFormatOptionForCell(0, 1, 'color')).toBeUndefined();
+      expect(model.getFormatOptionForCell(1, 0, 'color')).toBe('blue');
+      expect(model.getFormatOptionForCell(1, 1, 'color')).toBeUndefined();
+      (MOCK_BASE_MODEL.row as jest.Mock).mockClear();
+    });
+
+    test('returns undefined if no matching rule', () => {
+      const model = new UITableModel({
+        dh: MOCK_DH,
+        model: MOCK_BASE_MODEL,
+        table: MOCK_TABLE,
+        databars: [],
+        format: [{ cols: 'column0', color: 'red' }],
+      });
+      expect(model.getFormatOptionForCell(1, 1, 'color')).toBeUndefined();
+      expect(
+        model.getFormatOptionForCell(1, 1, 'background_color')
+      ).toBeUndefined();
+    });
+
+    test('returns undefined if condition data has not been fetched', () => {
+      (MOCK_BASE_MODEL.row as jest.Mock).mockImplementation(r => ({
+        data: null,
+      }));
+
+      const model = new UITableModel({
+        dh: MOCK_DH,
+        model: MOCK_BASE_MODEL,
+        table: MOCK_TABLE,
+        databars: [],
+        format: [{ color: 'red', if_: 'even' }],
+      });
+      expect(model.getFormatOptionForCell(0, 0, 'color')).toBeUndefined();
+      expect(model.getFormatOptionForCell(0, 1, 'color')).toBeUndefined();
+      (MOCK_BASE_MODEL.row as jest.Mock).mockClear();
+    });
+  });
+
+  describe('colorForCell', () => {
+    test('returns the color for a cell', () => {
+      const model = new UITableModel({
+        dh: MOCK_DH,
+        model: MOCK_BASE_MODEL,
+        table: MOCK_TABLE,
+        databars: [],
+        format: [{ color: 'red' }],
+      });
+      expect(model.colorForCell(0, 0, {} as IrisGridThemeType)).toBe('red');
+    });
+
+    test('returns undefined if no color for a cell', () => {
+      const model = new UITableModel({
+        dh: MOCK_DH,
+        model: MOCK_BASE_MODEL,
+        table: MOCK_TABLE,
+        databars: [],
+        format: [],
+      });
+      expect(model.colorForCell(0, 0, {} as IrisGridThemeType)).toBeUndefined();
+      expect(MOCK_BASE_MODEL.colorForCell).toHaveBeenCalledTimes(1);
+    });
+
+    test('returns grid theme white if no color and background color dark', () => {
+      jest
+        .spyOn(GridRenderer, 'getCachedColorIsDark')
+        .mockImplementation(() => true);
+      const model = new UITableModel({
+        dh: MOCK_DH,
+        model: MOCK_BASE_MODEL,
+        table: MOCK_TABLE,
+        databars: [],
+        format: [{ background_color: 'black' }],
+      });
+      expect(
+        model.colorForCell(0, 0, { white: 'white' } as IrisGridThemeType)
+      ).toBe('white');
+      jest.restoreAllMocks();
+    });
+
+    test('returns grid theme black if no color and background color light', () => {
+      jest
+        .spyOn(GridRenderer, 'getCachedColorIsDark')
+        .mockImplementation(() => false);
+      const model = new UITableModel({
+        dh: MOCK_DH,
+        model: MOCK_BASE_MODEL,
+        table: MOCK_TABLE,
+        databars: [],
+        format: [{ background_color: 'white' }],
+      });
+      expect(
+        model.colorForCell(0, 0, { black: 'black' } as IrisGridThemeType)
+      ).toBe('black');
+      jest.restoreAllMocks();
+    });
+
+    test('returns theme colors from color map', () => {
+      const model = new UITableModel({
+        dh: MOCK_DH,
+        model: MOCK_BASE_MODEL,
+        table: MOCK_TABLE,
+        databars: [],
+        format: [{ color: 'foo' }],
+      });
+      model.setColorMap(new Map([['foo', 'bar']]));
+      expect(model.colorForCell(0, 0, {} as IrisGridThemeType)).toBe('bar');
+    });
+  });
+
+  describe('backgroundColorForCell', () => {
+    test('returns undefined if no background_color for a cell', () => {
+      const model = new UITableModel({
+        dh: MOCK_DH,
+        model: MOCK_BASE_MODEL,
+        table: MOCK_TABLE,
+        databars: [],
+        format: [],
+      });
+      expect(
+        model.backgroundColorForCell(0, 0, {} as IrisGridThemeType)
+      ).toBeUndefined();
+      expect(MOCK_BASE_MODEL.backgroundColorForCell).toHaveBeenCalledTimes(1);
+    });
+
+    test('returns theme colors from color map', () => {
+      const model = new UITableModel({
+        dh: MOCK_DH,
+        model: MOCK_BASE_MODEL,
+        table: MOCK_TABLE,
+        databars: [],
+        format: [{ background_color: 'foo' }],
+      });
+      model.setColorMap(new Map([['foo', 'bar']]));
+      expect(model.backgroundColorForCell(0, 0, {} as IrisGridThemeType)).toBe(
+        'bar'
+      );
+    });
+  });
+});
diff --git a/plugins/ui/test/deephaven/ui/test_ui_table.py b/plugins/ui/test/deephaven/ui/test_ui_table.py
index f744c72a6..d858b5acf 100644
--- a/plugins/ui/test/deephaven/ui/test_ui_table.py
+++ b/plugins/ui/test/deephaven/ui/test_ui_table.py
@@ -158,67 +158,6 @@ def test_show_search(self):
             },
         )
 
-    def test_sort(self):
-        import deephaven.ui as ui
-        from deephaven import SortDirection
-
-        ui_table = ui.table(self.source)
-
-        t = ui_table.sort("X")
-        self.expect_render(
-            t,
-            {
-                "sorts": [{"column": "X", "direction": "ASC", "is_abs": False}],
-            },
-        )
-
-        t = ui_table.sort("X", SortDirection.DESCENDING)
-        self.expect_render(
-            t,
-            {
-                "sorts": [{"column": "X", "direction": "DESC", "is_abs": False}],
-            },
-        )
-
-        self.assertRaises(
-            ValueError, ui_table.sort, ["X", "Y"], [SortDirection.ASCENDING]
-        )
-
-        self.assertRaises(
-            ValueError,
-            ui_table.sort,
-            ["X"],
-            [SortDirection.ASCENDING, SortDirection.DESCENDING],
-        )
-
-        t = ui_table.sort(
-            ["X", "Y"], [SortDirection.ASCENDING, SortDirection.DESCENDING]
-        )
-
-        self.expect_render(
-            t,
-            {
-                "sorts": [
-                    {"column": "X", "direction": "ASC", "is_abs": False},
-                    {"column": "Y", "direction": "DESC", "is_abs": False},
-                ],
-            },
-        )
-
-        t = ui_table.sort(["X", "Y"], ["DESC", "ASC"])
-
-        self.expect_render(
-            t,
-            {
-                "sorts": [
-                    {"column": "X", "direction": "DESC", "is_abs": False},
-                    {"column": "Y", "direction": "ASC", "is_abs": False},
-                ],
-            },
-        )
-
-        self.assertRaises(ValueError, ui_table.sort, ["X", "Y"], ["INVALID"])
-
     def test_front_columns(self):
         import deephaven.ui as ui
 
diff --git a/tests/app.d/tests.app b/tests/app.d/tests.app
index b440c9fb0..d9c378984 100644
--- a/tests/app.d/tests.app
+++ b/tests/app.d/tests.app
@@ -7,4 +7,5 @@ file_0=express.py
 file_1=matplotlib.py
 file_2=ui.py
 file_3=ui_render_all.py
-file_4=ui_flex.py
\ No newline at end of file
+file_4=ui_flex.py
+file_5=ui_table.py
\ No newline at end of file
diff --git a/tests/app.d/ui_table.py b/tests/app.d/ui_table.py
new file mode 100644
index 000000000..2b02b7385
--- /dev/null
+++ b/tests/app.d/ui_table.py
@@ -0,0 +1,55 @@
+from deephaven import ui
+from deephaven import empty_table
+import deephaven.plot.express as dx
+
+_t = empty_table(100).update(["x = i", "y = sin(i)"])
+
+t_alignment = ui.table(
+    _t,
+    format_=[
+        ui.TableFormat(alignment="left"),
+        ui.TableFormat(cols="x", alignment="center"),
+    ],
+)
+
+t_background_color = ui.table(
+    _t,
+    format_=[
+        ui.TableFormat(cols="x", if_="x > 5", background_color="salmon"),
+        ui.TableFormat(cols="y", if_="y < 0", background_color="negative"),
+        ui.TableFormat(cols="y", if_="y > 0", background_color="positive"),
+    ],
+)
+
+t_color = ui.table(
+    _t,
+    format_=[
+        ui.TableFormat(background_color="subdued-content-bg"),
+        ui.TableFormat(
+            cols="x", if_="x > 5", color="lemonchiffon", background_color="salmon"
+        ),
+        ui.TableFormat(cols="y", if_="y < 0", color="negative"),
+        ui.TableFormat(cols="y", if_="y > 0", color="positive"),
+    ],
+)
+
+t_priority = ui.table(
+    _t,
+    format_=[
+        ui.TableFormat(background_color="accent-100"),
+        ui.TableFormat(background_color="accent-200", if_="x > 0"),
+        ui.TableFormat(background_color="accent-300", if_="x > 1"),
+        ui.TableFormat(background_color="accent-400", if_="x > 2"),
+        ui.TableFormat(background_color="accent-500", if_="x > 3"),
+        ui.TableFormat(background_color="accent-600", if_="x > 4"),
+    ],
+)
+
+t_value_format = ui.table(
+    dx.data.stocks(False),
+    format_=[
+        ui.TableFormat(value="0.00"),
+        ui.TableFormat(cols="Timestamp", value="MM/dd/yyyy"),
+        ui.TableFormat(cols=["Price", "Dollars"], value="$0.00"),
+    ],
+)
diff --git a/tests/ui_table.spec.ts b/tests/ui_table.spec.ts
new file mode 100644
index 000000000..d47dc11e2
--- /dev/null
+++ b/tests/ui_table.spec.ts
@@ -0,0 +1,22 @@
+import { expect, test } from '@playwright/test';
+import { openPanel, gotoPage } from './utils';
+
+const REACT_PANEL_VISIBLE = '.dh-react-panel:visible';
+
+// Tests flex components render as expected
+test.describe('UI flex components', () => {
+  [
+    't_alignment',
+    't_background_color',
+    't_color',
+    't_priority',
+    't_value_format',
+  ].forEach(name => {
+    test(name, async ({ page }) => {
+      await gotoPage(page, '');
+      await openPanel(page, name, REACT_PANEL_VISIBLE);
+
+      await expect(page.locator(REACT_PANEL_VISIBLE)).toHaveScreenshot();
+    });
+  });
+});
diff --git a/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-alignment-1-chromium-linux.png b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-alignment-1-chromium-linux.png
new file mode 100644
index 000000000..52600623b
Binary files /dev/null and b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-alignment-1-chromium-linux.png differ
diff --git a/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-alignment-1-firefox-linux.png b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-alignment-1-firefox-linux.png
new file mode 100644
index 000000000..16c9ad784
Binary files /dev/null and b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-alignment-1-firefox-linux.png differ
diff --git a/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-alignment-1-webkit-linux.png b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-alignment-1-webkit-linux.png
new file mode 100644
index 000000000..46416046c
Binary files /dev/null and b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-alignment-1-webkit-linux.png differ
diff --git a/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-background-color-1-chromium-linux.png b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-background-color-1-chromium-linux.png
new file mode 100644
index 000000000..1fedc79ac
Binary files /dev/null and b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-background-color-1-chromium-linux.png differ
diff --git a/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-background-color-1-firefox-linux.png b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-background-color-1-firefox-linux.png
new file mode 100644
index 000000000..abf839d97
Binary files /dev/null and b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-background-color-1-firefox-linux.png differ
diff --git a/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-background-color-1-webkit-linux.png b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-background-color-1-webkit-linux.png
new file mode 100644
index 000000000..7c32b6c79
Binary files /dev/null and b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-background-color-1-webkit-linux.png differ
diff --git a/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-color-1-chromium-linux.png b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-color-1-chromium-linux.png
new file mode 100644
index 000000000..4e059cbe4
Binary files /dev/null and b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-color-1-chromium-linux.png differ
diff --git a/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-color-1-firefox-linux.png b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-color-1-firefox-linux.png
new file mode 100644
index 000000000..a8c2aee1c
Binary files /dev/null and b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-color-1-firefox-linux.png differ
diff --git a/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-color-1-webkit-linux.png b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-color-1-webkit-linux.png
new file mode 100644
index 000000000..4fde883da
Binary files /dev/null and b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-color-1-webkit-linux.png differ
diff --git a/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-priority-1-chromium-linux.png b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-priority-1-chromium-linux.png
new file mode 100644
index 000000000..20507dd83
Binary files /dev/null and b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-priority-1-chromium-linux.png differ
diff --git a/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-priority-1-firefox-linux.png b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-priority-1-firefox-linux.png
new file mode 100644
index 000000000..2666bfcec
Binary files /dev/null and b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-priority-1-firefox-linux.png differ
diff --git a/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-priority-1-webkit-linux.png b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-priority-1-webkit-linux.png
new file mode 100644
index 000000000..dabd360b3
Binary files /dev/null and b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-priority-1-webkit-linux.png differ
diff --git a/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-value-format-1-chromium-linux.png b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-value-format-1-chromium-linux.png
new file mode 100644
index 000000000..355577044
Binary files /dev/null and b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-value-format-1-chromium-linux.png differ
diff --git a/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-value-format-1-firefox-linux.png b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-value-format-1-firefox-linux.png
new file mode 100644
index 000000000..ffb5f7492
Binary files /dev/null and b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-value-format-1-firefox-linux.png differ
diff --git a/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-value-format-1-webkit-linux.png b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-value-format-1-webkit-linux.png
new file mode 100644
index 000000000..68ae6e070
Binary files /dev/null and b/tests/ui_table.spec.ts-snapshots/UI-flex-components-t-value-format-1-webkit-linux.png differ