From 690117a018555b18548c2bc24a1996248d0f42ad Mon Sep 17 00:00:00 2001
From: Constance Martineau <constance.alyssa.martineau@gmail.com>
Date: Wed, 25 Mar 2026 18:30:39 -0400
Subject: [PATCH 1/3] Allow gray, black, and white color tokens in
 AIRFLOW__API__THEME
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Previously the Theme model only accepted tokens.colors.brand, forcing
users to use globalCss CSS-variable hacks to customize neutral surfaces,
borders, and the gray palette.

Introduce ThemeColors sub-model accepting brand, gray, black, and white —
all optional, with at least one required. Existing brand-only configs
continue to work unchanged.

Also regenerate the private UI OpenAPI spec and TypeScript types.
---
 airflow-core/docs/howto/customize-ui.rst      |  45 +++++-
 .../src/airflow/api_fastapi/common/types.py   |  38 +++--
 .../core_api/openapi/_private_ui.yaml         |  28 +---
 .../ui/openapi-gen/requests/schemas.gen.ts    |  25 +---
 .../ui/openapi-gen/requests/types.gen.ts      |  12 +-
 .../unit/api_fastapi/common/test_types.py     | 135 +++++++++++++++++-
 .../core_api/routes/ui/test_config.py         |  67 +++++++++
 7 files changed, 283 insertions(+), 67 deletions(-)

diff --git a/airflow-core/docs/howto/customize-ui.rst b/airflow-core/docs/howto/customize-ui.rst
index 24e4b03589000..3d696f52969b7 100644
--- a/airflow-core/docs/howto/customize-ui.rst
+++ b/airflow-core/docs/howto/customize-ui.rst
@@ -70,14 +70,18 @@ We can provide a JSON configuration to customize the UI.
 
 .. important::
 
-  - You can customize the ``brand`` color palette, ``globalCss`` and the navigation icon via ``icon`` (and ``icon_dark_mode``).
-  - You must supply ``50``-``950`` OKLCH color values for ``brand`` color.
-  - OKLCH colors must have next format ``oklch(l c h)`` For more info see :ref:`config:api__theme`
-  - There is also the ability to provide custom global CSS for a fine grained theme control.
+  - You can customize the ``brand``, ``gray``, ``black``, and ``white`` color tokens, ``globalCss``, and the navigation icon via ``icon`` (and ``icon_dark_mode``).
+  - All color tokens are **optional** — you can override any subset without supplying the others.
+  - ``brand`` and ``gray`` each accept an 11-shade scale with keys ``50``–``950``.
+  - ``black`` and ``white`` each accept a single color: ``{ "value": "oklch(...)" }``.
+  - OKLCH colors must use the format ``oklch(l c h)``. For more info see :ref:`config:api__theme`
+  - There is also the ability to provide custom global CSS for fine-grained theme control.
 
 .. note::
 
-  Modifying the ``brand`` color palette you also modify the navbar/sidebar.
+  Modifying the ``brand`` color palette also modifies the navbar/sidebar.
+  Modifying ``gray`` controls neutral surfaces and borders.
+  Modifying ``black`` and ``white`` controls the darkest and lightest surface colors.
 
 To customize the UI, simply:
 
@@ -180,6 +184,37 @@ Dark Mode
     }
   }'
 
+Customizing gray, black, and white tokens
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+You can override the neutral palette and surface colors independently of ``brand``. ``gray`` controls
+borders and neutral UI elements, while ``black`` and ``white`` control the darkest and lightest surface
+backgrounds. All fields are optional — supply only the tokens you want to change.
+
+.. code-block::
+
+  AIRFLOW__API__THEME='{
+    "tokens": {
+      "colors": {
+        "gray": {
+          "50":  { "value": "oklch(0.975 0.002 264.0)" },
+          "100": { "value": "oklch(0.950 0.003 264.0)" },
+          "200": { "value": "oklch(0.880 0.005 264.0)" },
+          "300": { "value": "oklch(0.780 0.008 264.0)" },
+          "400": { "value": "oklch(0.640 0.012 264.0)" },
+          "500": { "value": "oklch(0.520 0.015 264.0)" },
+          "600": { "value": "oklch(0.420 0.015 264.0)" },
+          "700": { "value": "oklch(0.340 0.012 264.0)" },
+          "800": { "value": "oklch(0.260 0.009 264.0)" },
+          "900": { "value": "oklch(0.200 0.007 264.0)" },
+          "950": { "value": "oklch(0.145 0.005 264.0)" }
+        },
+        "black": { "value": "oklch(0.220 0.025 288.6)" },
+        "white": { "value": "oklch(0.985 0.002 264.0)" }
+      }
+    }
+  }'
+
 Icon (SVG-only)
 ^^^^^^^^^^^^^^^
 
diff --git a/airflow-core/src/airflow/api_fastapi/common/types.py b/airflow-core/src/airflow/api_fastapi/common/types.py
index 269669c8ee50e..bd4176a9fd927 100644
--- a/airflow-core/src/airflow/api_fastapi/common/types.py
+++ b/airflow-core/src/airflow/api_fastapi/common/types.py
@@ -20,7 +20,7 @@
 from dataclasses import dataclass
 from datetime import timedelta
 from enum import Enum
-from typing import Annotated, Literal
+from typing import Annotated, Any, Literal
 
 from pydantic import (
     AfterValidator,
@@ -188,19 +188,35 @@ def serialize_model(self) -> str:
         return f"oklch({self.lightness} {self.chroma} {self.hue})"
 
 
+# Private type aliases for theme token shapes
+_ColorShade = dict[Literal["value"], OklchColor]
+_SHADE_LITERAL = Literal["50", "100", "200", "300", "400", "500", "600", "700", "800", "900", "950"]
+_ColorScale = dict[_SHADE_LITERAL, _ColorShade]
+
+
+class ThemeColors(BaseModel):
+    """Color tokens for the UI theme. All fields are optional; at least one must be provided."""
+
+    brand: _ColorScale | None = None
+    gray: _ColorScale | None = None
+    black: _ColorShade | None = None
+    white: _ColorShade | None = None
+
+    @model_validator(mode="after")
+    def check_at_least_one_color(self) -> ThemeColors:
+        if not any([self.brand, self.gray, self.black, self.white]):
+            raise ValueError("At least one color token must be provided: brand, gray, black, or white")
+        return self
+
+    @model_serializer(mode="wrap")
+    def serialize_model(self, handler: Any) -> dict:
+        return {k: v for k, v in handler(self).items() if v is not None}
+
+
 class Theme(BaseModel):
     """JSON to modify Chakra's theme."""
 
-    tokens: dict[
-        Literal["colors"],
-        dict[
-            Literal["brand"],
-            dict[
-                Literal["50", "100", "200", "300", "400", "500", "600", "700", "800", "900", "950"],
-                dict[Literal["value"], OklchColor],
-            ],
-        ],
-    ]
+    tokens: dict[Literal["colors"], ThemeColors]
     globalCss: dict[str, dict] | None = None
     icon: ThemeIconType = None
     icon_dark_mode: ThemeIconType = None
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
index 9e81e8344fb20..915e4d5430052 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
+++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
@@ -3273,30 +3273,7 @@ components:
       properties:
         tokens:
           additionalProperties:
-            additionalProperties:
-              additionalProperties:
-                additionalProperties:
-                  $ref: '#/components/schemas/OklchColor'
-                propertyNames:
-                  const: value
-                type: object
-              propertyNames:
-                enum:
-                - '50'
-                - '100'
-                - '200'
-                - '300'
-                - '400'
-                - '500'
-                - '600'
-                - '700'
-                - '800'
-                - '900'
-                - '950'
-              type: object
-            propertyNames:
-              const: brand
-            type: object
+            $ref: '#/components/schemas/ThemeColors'
           propertyNames:
             const: colors
           type: object
@@ -3324,6 +3301,9 @@ components:
       - tokens
       title: Theme
       description: JSON to modify Chakra's theme.
+    ThemeColors:
+      additionalProperties: true
+      type: object
     TokenType:
       type: string
       enum:
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index f47aa107aea24..a6cabf8c9d3f1 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -8896,25 +8896,7 @@ export const $Theme = {
     properties: {
         tokens: {
             additionalProperties: {
-                additionalProperties: {
-                    additionalProperties: {
-                        additionalProperties: {
-                            '$ref': '#/components/schemas/OklchColor'
-                        },
-                        propertyNames: {
-                            const: 'value'
-                        },
-                        type: 'object'
-                    },
-                    propertyNames: {
-                        enum: ['50', '100', '200', '300', '400', '500', '600', '700', '800', '900', '950']
-                    },
-                    type: 'object'
-                },
-                propertyNames: {
-                    const: 'brand'
-                },
-                type: 'object'
+                '$ref': '#/components/schemas/ThemeColors'
             },
             propertyNames: {
                 const: 'colors'
@@ -8966,6 +8948,11 @@ export const $Theme = {
     description: "JSON to modify Chakra's theme."
 } as const;
 
+export const $ThemeColors = {
+    additionalProperties: true,
+    type: 'object'
+} as const;
+
 export const $TokenType = {
     type: 'string',
     enum: ['api', 'cli'],
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 7423c08d42c05..14a88fd0ffaf9 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -2200,13 +2200,7 @@ export type TeamResponse = {
  */
 export type Theme = {
     tokens: {
-        [key: string]: {
-            [key: string]: {
-                [key: string]: {
-                    [key: string]: OklchColor;
-                };
-            };
-        };
+        [key: string]: ThemeColors;
     };
     globalCss?: {
     [key: string]: {
@@ -2217,6 +2211,10 @@ export type Theme = {
     icon_dark_mode?: string | null;
 };
 
+export type ThemeColors = {
+    [key: string]: unknown;
+};
+
 /**
  * Type of token to generate.
  */
diff --git a/airflow-core/tests/unit/api_fastapi/common/test_types.py b/airflow-core/tests/unit/api_fastapi/common/test_types.py
index c56e0a2e62995..3476f6a529394 100644
--- a/airflow-core/tests/unit/api_fastapi/common/test_types.py
+++ b/airflow-core/tests/unit/api_fastapi/common/test_types.py
@@ -19,7 +19,7 @@
 import pytest
 from pydantic import ValidationError
 
-from airflow.api_fastapi.common.types import OklchColor
+from airflow.api_fastapi.common.types import OklchColor, Theme, ThemeColors
 
 
 class TestOklchColor:
@@ -74,3 +74,136 @@ def test_invalid_oklch(self, input_str, error_message):
         with pytest.raises(ValidationError) as exc_info:
             OklchColor.model_validate(input_str)
         assert error_message in str(exc_info.value)
+
+
+# Shared test data for Theme/ThemeColors tests
+_BRAND_SCALE = {
+    "50": {"value": "oklch(0.975 0.007 298.0)"},
+    "100": {"value": "oklch(0.950 0.014 298.0)"},
+    "200": {"value": "oklch(0.900 0.023 298.0)"},
+    "300": {"value": "oklch(0.800 0.030 298.0)"},
+    "400": {"value": "oklch(0.680 0.038 298.0)"},
+    "500": {"value": "oklch(0.560 0.044 298.0)"},
+    "600": {"value": "oklch(0.460 0.048 298.0)"},
+    "700": {"value": "oklch(0.390 0.049 298.0)"},
+    "800": {"value": "oklch(0.328 0.050 298.0)"},
+    "900": {"value": "oklch(0.230 0.043 298.0)"},
+    "950": {"value": "oklch(0.155 0.035 298.0)"},
+}
+_GRAY_SCALE = {
+    "50": {"value": "oklch(0.975 0.002 264.0)"},
+    "100": {"value": "oklch(0.950 0.003 264.0)"},
+    "200": {"value": "oklch(0.880 0.005 264.0)"},
+    "300": {"value": "oklch(0.780 0.008 264.0)"},
+    "400": {"value": "oklch(0.640 0.012 264.0)"},
+    "500": {"value": "oklch(0.520 0.015 264.0)"},
+    "600": {"value": "oklch(0.420 0.015 264.0)"},
+    "700": {"value": "oklch(0.340 0.012 264.0)"},
+    "800": {"value": "oklch(0.260 0.009 264.0)"},
+    "900": {"value": "oklch(0.200 0.007 264.0)"},
+    "950": {"value": "oklch(0.145 0.005 264.0)"},
+}
+_BLACK_SHADE = {"value": "oklch(0.220 0.025 288.6)"}
+_WHITE_SHADE = {"value": "oklch(0.985 0.002 264.0)"}
+
+
+class TestThemeColors:
+    def test_brand_only(self):
+        colors = ThemeColors.model_validate({"brand": _BRAND_SCALE})
+        assert colors.brand is not None
+        assert colors.gray is None
+        assert colors.black is None
+        assert colors.white is None
+
+    def test_gray_only(self):
+        colors = ThemeColors.model_validate({"gray": _GRAY_SCALE})
+        assert colors.gray is not None
+        assert colors.brand is None
+
+    def test_black_and_white_only(self):
+        colors = ThemeColors.model_validate({"black": _BLACK_SHADE, "white": _WHITE_SHADE})
+        assert colors.black is not None
+        assert colors.white is not None
+        assert colors.brand is None
+        assert colors.gray is None
+
+    def test_all_tokens(self):
+        colors = ThemeColors.model_validate(
+            {"brand": _BRAND_SCALE, "gray": _GRAY_SCALE, "black": _BLACK_SHADE, "white": _WHITE_SHADE}
+        )
+        assert colors.brand is not None
+        assert colors.gray is not None
+        assert colors.black is not None
+        assert colors.white is not None
+
+    def test_empty_colors_rejected(self):
+        with pytest.raises(ValidationError) as exc_info:
+            ThemeColors.model_validate({})
+        assert "At least one color token must be provided" in str(exc_info.value)
+
+    def test_invalid_shade_key_rejected(self):
+        with pytest.raises(ValidationError):
+            ThemeColors.model_validate({"gray": {"999": {"value": "oklch(0.5 0.1 264.0)"}}})
+
+    def test_serialization_excludes_none_fields(self):
+        colors = ThemeColors.model_validate({"brand": _BRAND_SCALE})
+        dumped = colors.model_dump()
+        assert "brand" in dumped
+        assert "gray" not in dumped
+        assert "black" not in dumped
+        assert "white" not in dumped
+
+
+class TestTheme:
+    def test_brand_only_theme(self):
+        """Backwards-compatible: existing configs with only brand still work."""
+        theme = Theme.model_validate({"tokens": {"colors": {"brand": _BRAND_SCALE}}})
+        assert theme.tokens["colors"].brand is not None
+        assert theme.tokens["colors"].gray is None
+        assert theme.globalCss is None
+
+    def test_gray_only_theme(self):
+        """New: brand is no longer required."""
+        theme = Theme.model_validate({"tokens": {"colors": {"gray": _GRAY_SCALE}}})
+        assert theme.tokens["colors"].gray is not None
+        assert theme.tokens["colors"].brand is None
+
+    def test_black_white_theme(self):
+        theme = Theme.model_validate({"tokens": {"colors": {"black": _BLACK_SHADE, "white": _WHITE_SHADE}}})
+        assert theme.tokens["colors"].black is not None
+        assert theme.tokens["colors"].white is not None
+
+    def test_all_tokens_theme(self):
+        theme = Theme.model_validate(
+            {
+                "tokens": {
+                    "colors": {
+                        "brand": _BRAND_SCALE,
+                        "gray": _GRAY_SCALE,
+                        "black": _BLACK_SHADE,
+                        "white": _WHITE_SHADE,
+                    }
+                }
+            }
+        )
+        colors = theme.tokens["colors"]
+        assert colors.brand is not None
+        assert colors.gray is not None
+        assert colors.black is not None
+        assert colors.white is not None
+
+    def test_empty_colors_rejected(self):
+        with pytest.raises(ValidationError) as exc_info:
+            Theme.model_validate({"tokens": {"colors": {}}})
+        assert "At least one color token must be provided" in str(exc_info.value)
+
+    def test_serialization_round_trip(self):
+        """Verify None color fields are excluded and OklchColor values are serialized as strings."""
+        theme = Theme.model_validate({"tokens": {"colors": {"brand": _BRAND_SCALE}}})
+        dumped = theme.model_dump()
+        colors = dumped["tokens"]["colors"]
+        assert "brand" in colors
+        assert "gray" not in colors
+        assert "black" not in colors
+        assert "white" not in colors
+        assert colors["brand"]["50"]["value"] == "oklch(0.975 0.007 298.0)"
diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py
index ca5f44b1fc5b6..51c3230f97254 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py
@@ -92,6 +92,59 @@ def mock_config_data():
         yield
 
 
+THEME_WITH_ALL_COLORS = {
+    "tokens": {
+        "colors": {
+            "brand": {
+                "50": {"value": "oklch(0.975 0.008 298.0)"},
+                "100": {"value": "oklch(0.950 0.020 298.0)"},
+                "200": {"value": "oklch(0.900 0.045 298.0)"},
+                "300": {"value": "oklch(0.800 0.080 298.0)"},
+                "400": {"value": "oklch(0.680 0.120 298.0)"},
+                "500": {"value": "oklch(0.560 0.160 298.0)"},
+                "600": {"value": "oklch(0.460 0.190 298.0)"},
+                "700": {"value": "oklch(0.390 0.160 298.0)"},
+                "800": {"value": "oklch(0.328 0.080 298.0)"},
+                "900": {"value": "oklch(0.230 0.050 298.0)"},
+                "950": {"value": "oklch(0.155 0.030 298.0)"},
+            },
+            "gray": {
+                "50": {"value": "oklch(0.975 0.002 264.0)"},
+                "100": {"value": "oklch(0.950 0.003 264.0)"},
+                "200": {"value": "oklch(0.880 0.005 264.0)"},
+                "300": {"value": "oklch(0.780 0.008 264.0)"},
+                "400": {"value": "oklch(0.640 0.012 264.0)"},
+                "500": {"value": "oklch(0.520 0.015 264.0)"},
+                "600": {"value": "oklch(0.420 0.015 264.0)"},
+                "700": {"value": "oklch(0.340 0.012 264.0)"},
+                "800": {"value": "oklch(0.260 0.009 264.0)"},
+                "900": {"value": "oklch(0.200 0.007 264.0)"},
+                "950": {"value": "oklch(0.145 0.005 264.0)"},
+            },
+            "black": {"value": "oklch(0.220 0.025 288.6)"},
+            "white": {"value": "oklch(0.985 0.002 264.0)"},
+        }
+    },
+}
+
+
+@pytest.fixture
+def mock_config_data_all_colors():
+    with conf_vars(
+        {
+            ("api", "instance_name"): "Airflow",
+            ("api", "enable_swagger_ui"): "true",
+            ("api", "hide_paused_dags_by_default"): "true",
+            ("api", "fallback_page_limit"): "100",
+            ("api", "default_wrap"): "false",
+            ("api", "auto_refresh_interval"): "3",
+            ("api", "require_confirmation_dag_change"): "false",
+            ("api", "theme"): json.dumps(THEME_WITH_ALL_COLORS),
+        }
+    ):
+        yield
+
+
 class TestGetConfig:
     def test_should_response_200(self, mock_config_data, test_client):
         """
@@ -112,3 +165,17 @@ def test_get_config_just_authenticated(self, mock_config_data, unauthorized_test
         response = unauthorized_test_client.get("/config")
         assert response.status_code == 200
         assert response.json() == expected_config_response
+
+    def test_should_response_200_with_all_color_tokens(self, mock_config_data_all_colors, test_client):
+        """Theme with gray, black, and white tokens (in addition to brand) passes validation and round-trips."""
+        response = test_client.get("/config")
+
+        assert response.status_code == 200
+        theme = response.json()["theme"]
+        colors = theme["tokens"]["colors"]
+        assert "brand" in colors
+        assert "gray" in colors
+        assert "black" in colors
+        assert "white" in colors
+        assert colors["black"] == {"value": "oklch(0.22 0.025 288.6)"}
+        assert colors["white"] == {"value": "oklch(0.985 0.002 264.0)"}

From 44484300d1cea31d6b103c015558ba2c39cd191a Mon Sep 17 00:00:00 2001
From: Constance Martineau <constance.alyssa.martineau@gmail.com>
Date: Wed, 25 Mar 2026 18:31:49 -0400
Subject: [PATCH 2/3] Add newsfragment for #64232

---
 airflow-core/newsfragments/64232.feature.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 airflow-core/newsfragments/64232.feature.rst

diff --git a/airflow-core/newsfragments/64232.feature.rst b/airflow-core/newsfragments/64232.feature.rst
new file mode 100644
index 0000000000000..393a668ba8299
--- /dev/null
+++ b/airflow-core/newsfragments/64232.feature.rst
@@ -0,0 +1 @@
+Allow customizing gray, black, and white color tokens in AIRFLOW__API__THEME in addition to brand.

From e77e944a4a5a68f4fb0b1e0c82e75cae5b2bfbed Mon Sep 17 00:00:00 2001
From: Constance Martineau <constance.alyssa.martineau@gmail.com>
Date: Thu, 26 Mar 2026 16:05:49 -0400
Subject: [PATCH 3/3] Factorize common conf_vars into helper in test_config

---
 .../core_api/routes/ui/test_config.py         | 39 +++++++------------
 1 file changed, 15 insertions(+), 24 deletions(-)

diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py
index 51c3230f97254..dbc3c0eb64937 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py
@@ -72,23 +72,25 @@
 }
 
 
+def _theme_conf_vars(theme: dict) -> dict:
+    return {
+        ("api", "instance_name"): "Airflow",
+        ("api", "enable_swagger_ui"): "true",
+        ("api", "hide_paused_dags_by_default"): "true",
+        ("api", "fallback_page_limit"): "100",
+        ("api", "default_wrap"): "false",
+        ("api", "auto_refresh_interval"): "3",
+        ("api", "require_confirmation_dag_change"): "false",
+        ("api", "theme"): json.dumps(theme),
+    }
+
+
 @pytest.fixture
 def mock_config_data():
     """
     Mock configuration settings used in the endpoint.
     """
-    with conf_vars(
-        {
-            ("api", "instance_name"): "Airflow",
-            ("api", "enable_swagger_ui"): "true",
-            ("api", "hide_paused_dags_by_default"): "true",
-            ("api", "fallback_page_limit"): "100",
-            ("api", "default_wrap"): "false",
-            ("api", "auto_refresh_interval"): "3",
-            ("api", "require_confirmation_dag_change"): "false",
-            ("api", "theme"): json.dumps(THEME),
-        }
-    ):
+    with conf_vars(_theme_conf_vars(THEME)):
         yield
 
 
@@ -130,18 +132,7 @@ def mock_config_data():
 
 @pytest.fixture
 def mock_config_data_all_colors():
-    with conf_vars(
-        {
-            ("api", "instance_name"): "Airflow",
-            ("api", "enable_swagger_ui"): "true",
-            ("api", "hide_paused_dags_by_default"): "true",
-            ("api", "fallback_page_limit"): "100",
-            ("api", "default_wrap"): "false",
-            ("api", "auto_refresh_interval"): "3",
-            ("api", "require_confirmation_dag_change"): "false",
-            ("api", "theme"): json.dumps(THEME_WITH_ALL_COLORS),
-        }
-    ):
+    with conf_vars(_theme_conf_vars(THEME_WITH_ALL_COLORS)):
         yield
 
 
