You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by sb...@apache.org on 2026/08/07 23:01:03 UTC

(superset) branch master updated: feat: Color Picker replaces the color selection drop-down list in conditional formatting (#42053)

This is an automated email from the ASF dual-hosted git repository.

SBIN2010 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new b18015f63bf feat: Color Picker replaces the color selection drop-down list in conditional formatting (#42053)
b18015f63bf is described below

commit b18015f63bf791f973476491c30aa2cfc2874400
Author: SBIN2010 <Sb...@mail.ru>
AuthorDate: Sat Aug 8 02:00:56 2026 +0300

    feat: Color Picker replaces the color selection drop-down list in conditional formatting (#42053)
    
    Co-authored-by: rusackas <ev...@rusackas.com>
    Co-authored-by: Claude <no...@anthropic.com>
---
 .../superset-ui-chart-controls/src/types.ts        |   3 +-
 .../src/utils/getColorFormatters.ts                |  46 +++-
 .../test/utils/getColorFormatters.test.ts          | 164 ++++++++++++++
 .../packages/superset-ui-core/src/color/utils.ts   |  43 ++++
 .../superset-ui-core/test/color/utils.test.ts      |  49 ++++
 .../src/controlPanel.tsx                           |   8 +-
 .../test/controlPanel.test.tsx                     |   8 +-
 .../src/plugin/transformProps.ts                   |  17 +-
 .../plugins/plugin-chart-table/src/TableChart.tsx  |   5 +-
 .../plugin-chart-table/src/controlPanel.tsx        |   9 +-
 .../plugin-chart-table/src/transformProps.ts       |  11 +-
 .../plugin-chart-table/test/controlPanel.test.tsx  |   8 +-
 .../controls/ColorPickerControl.test.tsx           | 246 ++++++++++++++++-----
 .../components/controls/ColorPickerControl.tsx     | 193 ++++++++++++++--
 .../FormattingPopoverContent.test.tsx              |  94 ++++++--
 .../FormattingPopoverContent.tsx                   |  27 ++-
 .../ConditionalFormattingControl/constants.ts      |  16 +-
 .../controls/ConditionalFormattingControl/types.ts |   7 +-
 18 files changed, 816 insertions(+), 138 deletions(-)

diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
index 68ae08f93e1..2fa14a7997e 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts
@@ -36,6 +36,7 @@ import type {
   QueryResponse,
   TimeFormatter,
 } from '@superset-ui/core';
+import { type RGBColor } from '@superset-ui/core/components';
 import { GenericDataType } from '@apache-superset/core/common';
 import { sharedControls, sharedControlComponents } from './shared-controls';
 
@@ -494,7 +495,7 @@ export type ConditionalFormattingConfig = {
   targetValueLeft?: number;
   targetValueRight?: number;
   column?: string;
-  colorScheme?: string;
+  colorScheme?: RGBColor | string;
   toAllRow?: boolean;
   toTextColor?: boolean;
   useGradient?: boolean;
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts b/superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts
index 730d679c894..0b1ff0e6ce6 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts
@@ -19,7 +19,7 @@
 import memoizeOne from 'memoize-one';
 import { isString, isBoolean } from 'lodash-es';
 import { isBlank } from '@apache-superset/core/utils';
-import { addAlpha, DataRecord } from '@superset-ui/core';
+import { addAlpha, DataRecord, rgbaToHex } from '@superset-ui/core';
 import tinycolor from 'tinycolor2';
 import {
   ColorFormatters,
@@ -27,6 +27,7 @@ import {
   ConditionalFormattingConfig,
   MultipleValueComparators,
   ResolvedColorFormatterResult,
+  ColorSchemeEnum,
 } from '../types';
 
 export const round = (num: number, precision = 0) =>
@@ -71,6 +72,9 @@ export const getOpacity = (
   );
 };
 
+const isSpecialColor = (value: unknown): value is ColorSchemeEnum =>
+  Object.values(ColorSchemeEnum).includes(value as ColorSchemeEnum);
+
 export const getColorFunction = (
   {
     operator,
@@ -270,19 +274,51 @@ export const getColorFunction = (
     if (compareResult === false) return undefined;
     const { cutoffValue, extremeValue } = compareResult;
 
-    // If useGradient is explicitly false, return solid color
-    if (useGradient === false) {
+    if (typeof colorScheme === 'string') {
+      if (isSpecialColor(colorScheme)) {
+        return colorScheme;
+      }
+
+      if (
+        useGradient === false ||
+        (useGradient === undefined && colorScheme.length === 9)
+      ) {
+        if (alpha === false) {
+          return colorScheme.length === 9
+            ? colorScheme.slice(0, 7)
+            : colorScheme;
+        }
+        return colorScheme;
+      }
+
+      const cleanHex =
+        colorScheme.length === 9 ? colorScheme.slice(0, 7) : colorScheme;
+
+      if (alpha === undefined || alpha) {
+        return addAlpha(
+          cleanHex,
+          getOpacity(value, cutoffValue, extremeValue, minOpacity, maxOpacity),
+        );
+      }
       return colorScheme;
     }
+    // If useGradient is explicitly false, return solid color
+    if (useGradient === false || useGradient === undefined) {
+      if (alpha === false) {
+        return rgbaToHex({ ...colorScheme, a: 1 });
+      }
+      return rgbaToHex(colorScheme);
+    }
 
+    const baseHexColor = rgbaToHex({ ...colorScheme, a: 1 });
     // Otherwise apply gradient (default behavior for backward compatibility)
     if (alpha === undefined || alpha) {
       return addAlpha(
-        colorScheme,
+        baseHexColor,
         getOpacity(value, cutoffValue, extremeValue, minOpacity, maxOpacity),
       );
     }
-    return colorScheme;
+    return baseHexColor;
   };
 };
 
diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts b/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts
index 821a864a546..4e3ab26e4c4 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts
+++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts
@@ -952,3 +952,167 @@ test('correct column boolean config', () => {
   expect(colorFormatters[3].getColorFromValue(true)).toEqual('#FF0000FF');
   expect(colorFormatters[3].getColorFromValue(false)).toEqual('#FF0000FF');
 });
+
+test('should return hex color when colorScheme is an RGB object', () => {
+  const colorFunction = getColorFunction(
+    {
+      operator: Comparator.None,
+      colorScheme: { r: 255, g: 128, b: 0, a: 1 },
+      column: 'name',
+    },
+    strValues,
+  );
+  expect(colorFunction('Diana')).toEqual('#ff8000');
+  expect(colorFunction('Carlos')).toEqual('#ff8000');
+  expect(colorFunction('Brian')).toEqual('#ff8000');
+});
+
+test('should return token name as-is when colorScheme is a string token', () => {
+  const colorFunction = getColorFunction(
+    {
+      operator: Comparator.None,
+      colorScheme: 'Green',
+      column: 'name',
+    },
+    strValues,
+  );
+  expect(colorFunction('Diana')).toEqual('Green');
+  expect(colorFunction('Carlos')).toEqual('Green');
+  expect(colorFunction('Brian')).toEqual('Green');
+});
+
+test('should return solid hex color when useGradient is false or true', () => {
+  const columnConfig = [
+    {
+      operator: Comparator.GreaterThan,
+      targetValue: 50,
+      colorScheme: { r: 0, g: 47, b: 255, a: 1 },
+      column: 'count',
+      useGradient: false,
+    },
+    {
+      operator: Comparator.GreaterThan,
+      targetValue: 50,
+      colorScheme: { r: 255, g: 166, b: 0, a: 1 },
+      column: 'count',
+      useGradient: true,
+    },
+  ];
+  const colorFormatters = getColorFormatters(columnConfig, mockData);
+  expect(colorFormatters.length).toEqual(2);
+
+  // First formatter with useGradient: false should return solid color
+  expect(colorFormatters[0].column).toEqual('count');
+  expect(colorFormatters[0].getColorFromValue(100)).toEqual('#002fff');
+
+  // Second formatter with useGradient: true should return gradient color
+  expect(colorFormatters[1].column).toEqual('count');
+  expect(colorFormatters[1].getColorFromValue(100)).toEqual('#ffa600FF');
+});
+
+test('should return hex color without alpha for GreaterThan operator with RGB colorScheme', () => {
+  const config = {
+    operator: Comparator.GreaterThan,
+    targetValue: 50,
+    colorScheme: { r: 255, g: 0, b: 0, a: 1 },
+    useGradient: true,
+  };
+
+  const columnValues = [10, 50, 100];
+
+  const alpha = false;
+  const colorFunction = getColorFunction(config, columnValues, alpha);
+
+  expect(colorFunction(100)).toEqual('#ff0000');
+});
+
+test('should preserve alpha from colorScheme when useGradient is false', () => {
+  const config = {
+    operator: Comparator.None,
+    colorScheme: { r: 255, g: 0, b: 0, a: 0.5 },
+    useGradient: false,
+  };
+
+  const colorFunction = getColorFunction(config, [10, 20, 30]);
+  const result = colorFunction(20);
+
+  expect(result).not.toBe('#ff0000');
+  expect(result).not.toBe('rgb(255, 0, 0)');
+});
+
+test('should force opaque color when useGradient is false but alpha is explicitly false', () => {
+  const config = {
+    operator: Comparator.None,
+    colorScheme: { r: 255, g: 0, b: 0, a: 0.5 },
+    useGradient: false,
+  };
+
+  const colorFunction = getColorFunction(config, [10, 20, 30], false);
+  const result = colorFunction(20);
+
+  expect(result).toBe('#ff0000');
+});
+
+test('should return colorScheme as-is when alpha is false and length is 7', () => {
+  const colorFunction = getColorFunction(
+    {
+      operator: Comparator.GreaterThan,
+      targetValue: 50,
+      colorScheme: '#FF0000',
+      useGradient: false,
+      column: 'count',
+    },
+    countValues,
+    false,
+  );
+
+  expect(colorFunction(100)).toEqual('#FF0000');
+});
+
+test('should preserve alpha when alpha is undefined and colorScheme has 9 chars', () => {
+  const colorFunction = getColorFunction(
+    {
+      operator: Comparator.GreaterThan,
+      targetValue: 50,
+      colorScheme: '#FF000080',
+      useGradient: false,
+      column: 'count',
+    },
+    countValues,
+  );
+
+  expect(colorFunction(100)).toEqual('#FF000080');
+});
+
+test('should preserve alpha when alpha is true and colorScheme has 9 chars', () => {
+  const colorFunction = getColorFunction(
+    {
+      operator: Comparator.GreaterThan,
+      targetValue: 50,
+      colorScheme: '#FF000080',
+      useGradient: false,
+      column: 'count',
+    },
+    countValues,
+    true,
+  );
+
+  expect(colorFunction(100)).toEqual('#FF000080');
+});
+
+test('should strip alpha channel when alpha is false and colorScheme has 9 chars', () => {
+  const colorFunction = getColorFunction(
+    {
+      operator: Comparator.GreaterThan,
+      targetValue: 50,
+      colorScheme: '#FF000080',
+      useGradient: false,
+      column: 'count',
+    },
+    countValues,
+    false,
+  );
+
+  expect(colorFunction(100)).toEqual('#FF0000');
+  expect(colorFunction(100)).toHaveLength(7);
+});
diff --git a/superset-frontend/packages/superset-ui-core/src/color/utils.ts b/superset-frontend/packages/superset-ui-core/src/color/utils.ts
index c0f33f9f8c1..58f1aa70742 100644
--- a/superset-frontend/packages/superset-ui-core/src/color/utils.ts
+++ b/superset-frontend/packages/superset-ui-core/src/color/utils.ts
@@ -17,6 +17,7 @@
  * under the License.
  */
 import tinycolor from 'tinycolor2';
+import { type RGBColor } from '@superset-ui/core/components';
 
 const rgbRegex = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/;
 export function getContrastingColor(color: string, thresholds = 186) {
@@ -120,3 +121,45 @@ export function rgbToHex(red: number, green: number, blue: number) {
 
   return `#${r}${g}${b}`;
 }
+
+export function rgbaToHex(rgb: RGBColor): string {
+  const { r, g, b, a = 1 } = rgb;
+  const clampChannel = (value: number) =>
+    Math.min(255, Math.max(0, Math.round(value)));
+  const clampAlpha = (value: number) => Math.min(1, Math.max(0, value));
+  const toHex = (value: number) => {
+    const hex = value.toString(16);
+    return hex.length === 1 ? `0${hex}` : hex;
+  };
+  const hexColor = `#${toHex(clampChannel(r))}${toHex(clampChannel(g))}${toHex(clampChannel(b))}`;
+  const clampedAlpha = clampAlpha(a);
+  if (clampedAlpha !== 1) {
+    return `${hexColor}${toHex(Math.round(clampedAlpha * 255))}`;
+  }
+  return hexColor;
+}
+
+export const forceHexAlpha = (color: string | RGBColor): string => {
+  if (typeof color === 'object' && color !== null) {
+    return rgbaToHex({ ...color, a: 0.6 });
+  }
+
+  let hex = color.startsWith('#') ? color : `#${color}`;
+
+  // Expand shorthand hex (#rgb, #rgba) to full length before appending or
+  // replacing the alpha channel, otherwise the result is not a valid 6- or
+  // 8-digit CSS hex color.
+  if (hex.length === 4 || hex.length === 5) {
+    hex = `#${hex
+      .slice(1)
+      .split('')
+      .map(char => char + char)
+      .join('')}`;
+  }
+
+  if (hex.length === 9) {
+    return `${hex.slice(0, -2)}99`;
+  }
+
+  return `${hex}99`;
+};
diff --git a/superset-frontend/packages/superset-ui-core/test/color/utils.test.ts b/superset-frontend/packages/superset-ui-core/test/color/utils.test.ts
index e481a1d7e80..f98357abf5a 100644
--- a/superset-frontend/packages/superset-ui-core/test/color/utils.test.ts
+++ b/superset-frontend/packages/superset-ui-core/test/color/utils.test.ts
@@ -22,6 +22,8 @@ import {
   addAlpha,
   hexToRgb,
   rgbToHex,
+  rgbaToHex,
+  forceHexAlpha,
 } from '@superset-ui/core';
 
 describe('color utils', () => {
@@ -106,4 +108,51 @@ describe('color utils', () => {
       expect(rgbToHex(0, 0, 0)).toBe('#000000');
     });
   });
+  describe('rgbaToHex', () => {
+    test('omits the alpha channel for opaque colors', () => {
+      expect(rgbaToHex({ r: 255, g: 0, b: 0 })).toBe('#ff0000');
+      expect(rgbaToHex({ r: 255, g: 0, b: 0, a: 1 })).toBe('#ff0000');
+    });
+    test('appends the alpha channel for translucent colors', () => {
+      expect(rgbaToHex({ r: 0, g: 150, b: 0, a: 0.2 })).toBe('#00960033');
+      expect(rgbaToHex({ r: 0, g: 0, b: 0, a: 0.5 })).toBe('#00000080');
+    });
+    test('fully transparent colors keep an explicit 00 alpha', () => {
+      expect(rgbaToHex({ r: 255, g: 255, b: 255, a: 0 })).toBe('#ffffff00');
+    });
+    test('zero-pads single-digit channels', () => {
+      expect(rgbaToHex({ r: 1, g: 2, b: 3 })).toBe('#010203');
+    });
+    test('rounds fractional channel values', () => {
+      expect(rgbaToHex({ r: 254.6, g: 0.4, b: 0 })).toBe('#ff0000');
+    });
+    test('clamps out-of-range channel and alpha values', () => {
+      expect(rgbaToHex({ r: 300, g: -10, b: 0 })).toBe('#ff0000');
+      expect(rgbaToHex({ r: 0, g: 0, b: 0, a: 1.5 })).toBe('#000000');
+      expect(rgbaToHex({ r: 0, g: 0, b: 0, a: -0.5 })).toBe('#00000000');
+    });
+  });
+  describe('forceHexAlpha', () => {
+    test('appends 60% alpha to a 6-digit hex string', () => {
+      expect(forceHexAlpha('#ff0000')).toBe('#ff000099');
+    });
+    test('adds the # prefix when missing', () => {
+      expect(forceHexAlpha('ff0000')).toBe('#ff000099');
+    });
+    test('replaces the existing alpha on an 8-digit hex string', () => {
+      expect(forceHexAlpha('#ff000033')).toBe('#ff000099');
+    });
+    test('converts an RGBColor object using 60% alpha', () => {
+      expect(forceHexAlpha({ r: 255, g: 0, b: 0 })).toBe('#ff000099');
+    });
+    test('overrides the alpha of a translucent RGBColor object', () => {
+      expect(forceHexAlpha({ r: 0, g: 150, b: 0, a: 0.2 })).toBe('#00960099');
+    });
+    test('expands a shorthand 3-digit hex string before adding alpha', () => {
+      expect(forceHexAlpha('#fff')).toBe('#ffffff99');
+    });
+    test('expands a shorthand 4-digit hex string before replacing alpha', () => {
+      expect(forceHexAlpha('#ff03')).toBe('#ffff0099');
+    });
+  });
 });
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/controlPanel.tsx
index 3ef4ee27a40..e95bb121242 100644
--- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/controlPanel.tsx
@@ -707,12 +707,8 @@ const config: ControlPanelConfig = {
                 const extraColorChoices = hasTimeComparison
                   ? [
                       {
-                        value: ColorSchemeEnum.Green,
-                        label: t('Green for increase, red for decrease'),
-                      },
-                      {
-                        value: ColorSchemeEnum.Red,
-                        label: t('Red for increase, green for decrease'),
+                        label: t('Trend colors'),
+                        colors: [ColorSchemeEnum.Green, ColorSchemeEnum.Red],
                       },
                     ]
                   : [];
diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/test/controlPanel.test.tsx b/superset-frontend/plugins/plugin-chart-ag-grid-table/test/controlPanel.test.tsx
index 68c34b5f98e..9819281390c 100644
--- a/superset-frontend/plugins/plugin-chart-ag-grid-table/test/controlPanel.test.tsx
+++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/test/controlPanel.test.tsx
@@ -154,12 +154,8 @@ test('extraColorChoices included when time comparison is enabled', () => {
 
   expect(result.extraColorChoices).toEqual([
     {
-      value: ColorSchemeEnum.Green,
-      label: expect.stringContaining('Green for increase'),
-    },
-    {
-      value: ColorSchemeEnum.Red,
-      label: expect.stringContaining('Red for increase'),
+      label: expect.stringContaining('Trend colors'),
+      colors: [ColorSchemeEnum.Green, ColorSchemeEnum.Red],
     },
   ]);
   expect(result.columnOptions).not.toEqual(
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts
index ab94f125e89..61ed7f7244c 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/plugin/transformProps.ts
@@ -30,7 +30,11 @@ import {
   TimeFormats,
 } from '@superset-ui/core';
 import { GenericDataType } from '@apache-superset/core/common';
-import { getColorFormatters } from '@superset-ui/chart-controls';
+import {
+  ColorSchemeEnum,
+  ConditionalFormattingConfig,
+  getColorFormatters,
+} from '@superset-ui/chart-controls';
 import { DateFormatter, PivotTableQueryFormData, QueryData } from '../types';
 import buildGroupbyCombinations, {
   additiveReducerFor,
@@ -206,8 +210,17 @@ export default function transformProps(chartProps: ChartProps<QueryFormData>) {
       },
       {},
     );
+  // The "Green"/"Red" trend-color tokens are resolved by the Table chart's
+  // own comparison-aware formatter, which this renderer does not implement.
+  // Filter them out so a stale config (e.g. carried over from switching viz
+  // types) doesn't leak the raw token name through as a literal CSS color.
+  const pivotConditionalFormatting = conditionalFormatting?.filter(
+    (config: ConditionalFormattingConfig) =>
+      config.colorScheme !== ColorSchemeEnum.Green &&
+      config.colorScheme !== ColorSchemeEnum.Red,
+  );
   const metricColorFormatters = getColorFormatters(
-    conditionalFormatting,
+    pivotConditionalFormatting,
     mainQuery.data,
     theme,
   );
diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
index 65bbc5b3702..1f6d634e597 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx
@@ -46,6 +46,7 @@ import {
   BinaryQueryObjectFilterClause,
   extractTextFromHTML,
   TimeGranularity,
+  forceHexAlpha,
 } from '@superset-ui/core';
 import {
   styled,
@@ -1094,7 +1095,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
                 formatter.objectFormatting === ObjectFormattingEnum.CELL_BAR
               ) {
                 if (generalShowCellBars)
-                  backgroundColorCellBar = formatterResult.slice(0, -2);
+                  backgroundColorCellBar = forceHexAlpha(formatterResult);
               } else {
                 backgroundColor = formatterResult;
                 valueRangeFlag = false;
@@ -1182,7 +1183,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
                   alignPositiveNegative,
                 })}%`};
                 background-color: ${
-                  (backgroundColorCellBar && `${backgroundColorCellBar}99`) ||
+                  backgroundColorCellBar ||
                   cellBackground({
                     value: value as number,
                     colorPositiveNegative,
diff --git a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
index 5a81e4ff526..50e468195b5 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
@@ -757,12 +757,8 @@ const config: ControlPanelConfig = {
                 const extraColorChoices = hasTimeComparison
                   ? [
                       {
-                        value: ColorSchemeEnum.Green,
-                        label: t('Green for increase, red for decrease'),
-                      },
-                      {
-                        value: ColorSchemeEnum.Red,
-                        label: t('Red for increase, green for decrease'),
+                        label: t('Trend colors'),
+                        colors: [ColorSchemeEnum.Green, ColorSchemeEnum.Red],
                       },
                     ]
                   : [];
@@ -774,6 +770,7 @@ const config: ControlPanelConfig = {
                     (item: ConditionalFormattingConfig, index, array) => {
                       if (
                         item.colorScheme &&
+                        typeof item.colorScheme === 'string' &&
                         !['Green', 'Red'].includes(item.colorScheme)
                       ) {
                         if (item.columnFormatting === undefined) {
diff --git a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
index e8a15d4a67d..d71eec62135 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
@@ -752,8 +752,15 @@ const transformProps = (
   const basicColorFormatters =
     comparisonColorEnabled && getBasicColorFormatter(baseQuery?.data, columns);
   const columnColorFormatters =
-    getColorFormatters(conditionalFormatting, passedData, theme) ??
-    defaultColorFormatters;
+    getColorFormatters(
+      (conditionalFormatting || []).filter(
+        (config: ConditionalFormattingConfig) =>
+          config.colorScheme !== ColorSchemeEnum.Green &&
+          config.colorScheme !== ColorSchemeEnum.Red,
+      ),
+      passedData,
+      theme,
+    ) ?? defaultColorFormatters;
 
   const basicColorColumnFormatters = getBasicColorFormatterForColumn(
     baseQuery?.data,
diff --git a/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.tsx b/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.tsx
index 9c45037683f..dadeaa34fe3 100644
--- a/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.tsx
+++ b/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.tsx
@@ -156,12 +156,8 @@ test('extraColorChoices included when time comparison is enabled', () => {
 
   expect(result.extraColorChoices).toEqual([
     {
-      value: ColorSchemeEnum.Green,
-      label: expect.stringContaining('Green for increase'),
-    },
-    {
-      value: ColorSchemeEnum.Red,
-      label: expect.stringContaining('Red for increase'),
+      label: expect.stringContaining('Trend colors'),
+      colors: [ColorSchemeEnum.Green, ColorSchemeEnum.Red],
     },
   ]);
   expect(result.columnOptions).not.toEqual(
diff --git a/superset-frontend/src/explore/components/controls/ColorPickerControl.test.tsx b/superset-frontend/src/explore/components/controls/ColorPickerControl.test.tsx
index d39200da33e..77e7630236c 100644
--- a/superset-frontend/src/explore/components/controls/ColorPickerControl.test.tsx
+++ b/superset-frontend/src/explore/components/controls/ColorPickerControl.test.tsx
@@ -16,7 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { render, screen, userEvent } from 'spec/helpers/testing-library';
+import {
+  render,
+  screen,
+  userEvent,
+  waitFor,
+} from 'spec/helpers/testing-library';
 import {
   CategoricalScheme,
   getCategoricalSchemeRegistry,
@@ -28,67 +33,208 @@ const defaultProps = {
   onChange: jest.fn(),
 };
 
-// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
-describe('ColorPickerControl', () => {
-  beforeAll(() => {
-    getCategoricalSchemeRegistry()
-      .registerValue(
-        'test',
-        new CategoricalScheme({
-          id: 'test',
-          colors: ['#ff0000', '#00ff00', '#0000ff'],
-        }),
-      )
-      .setDefaultKey('test');
-  });
+beforeAll(() => {
+  getCategoricalSchemeRegistry()
+    .registerValue(
+      'test',
+      new CategoricalScheme({
+        id: 'test',
+        colors: ['#ff0000', '#00ff00', '#0000ff'],
+      }),
+    )
+    .setDefaultKey('test');
+});
 
-  beforeEach(() => {
-    jest.clearAllMocks();
-  });
+beforeEach(() => {
+  jest.clearAllMocks();
+});
 
-  test('renders a ColorPicker component', () => {
-    render(<ColorPickerControl {...defaultProps} />);
+test('renders a ColorPicker component', () => {
+  render(<ColorPickerControl {...defaultProps} />);
 
-    // AntD ColorPicker renders a trigger element with class
-    const colorPickerTrigger = document.querySelector(
-      '.ant-color-picker-trigger',
-    );
-    expect(colorPickerTrigger).toBeInTheDocument();
-  });
+  // AntD ColorPicker renders a trigger element with class
+  const colorPickerTrigger = document.querySelector(
+    '.ant-color-picker-trigger',
+  );
+  expect(colorPickerTrigger).toBeInTheDocument();
+});
+
+test('displays the correct color value', () => {
+  render(<ColorPickerControl {...defaultProps} />);
+
+  // The color should be displayed as hex #007A87 (uppercase in AntD)
+  expect(screen.getByText('#007A87')).toBeInTheDocument();
+});
+
+test('calls onChange with RGB values when color changes', async () => {
+  const onChange = jest.fn();
+  render(<ColorPickerControl {...defaultProps} onChange={onChange} />);
 
-  test('displays the correct color value', () => {
-    render(<ColorPickerControl {...defaultProps} />);
+  // Open the color picker
+  const colorPickerTrigger = document.querySelector(
+    '.ant-color-picker-trigger',
+  );
+  expect(colorPickerTrigger).toBeInTheDocument();
 
-    // The color should be displayed as hex #007A87 (uppercase in AntD)
-    expect(screen.getByText('#007A87')).toBeInTheDocument();
+  if (colorPickerTrigger) {
+    await userEvent.click(colorPickerTrigger);
+  }
+
+  // Note: Testing actual color selection in AntD ColorPicker would require more complex mocking
+  // as it uses complex internal components. The main functionality is covered by the component itself.
+});
+
+test('includes preset colors from the categorical scheme', () => {
+  render(<ColorPickerControl {...defaultProps} />);
+
+  // The component should have access to the preset colors from the registry
+  // This is tested by ensuring the component renders without errors with the presets
+  const colorPickerTrigger = document.querySelector(
+    '.ant-color-picker-trigger',
+  );
+  expect(colorPickerTrigger).toBeInTheDocument();
+});
+
+test('calls onChange with string key "Green" when resolveThemeTokens is true', async () => {
+  const onChange = jest.fn();
+
+  render(
+    <ColorPickerControl
+      {...defaultProps}
+      onChange={onChange}
+      resolveThemeTokens
+      presets={[{ label: 'Special Colors', colors: ['Green', 'Red'] }]}
+    />,
+  );
+
+  const colorPickerTrigger = document.querySelector(
+    '.ant-color-picker-trigger',
+  );
+  expect(colorPickerTrigger).toBeInTheDocument();
+  await userEvent.click(colorPickerTrigger!);
+
+  await waitFor(() => {
+    expect(
+      document.querySelector('.ant-color-picker-presets-color'),
+    ).toBeInTheDocument();
   });
 
-  test('calls onChange with RGB values when color changes', async () => {
-    const onChange = jest.fn();
-    render(<ColorPickerControl {...defaultProps} onChange={onChange} />);
+  const presets = document.querySelectorAll('.ant-color-picker-presets-color');
+  const greenPreset = presets[0];
+
+  expect(greenPreset).toBeInTheDocument();
+  await userEvent.click(greenPreset);
+
+  expect(onChange).toHaveBeenCalledWith('Green');
+});
+
+test('calls onChange with RGB object when resolveThemeTokens is false', async () => {
+  const onChange = jest.fn();
 
-    // Open the color picker
-    const colorPickerTrigger = document.querySelector(
-      '.ant-color-picker-trigger',
-    );
-    expect(colorPickerTrigger).toBeInTheDocument();
+  render(
+    <ColorPickerControl
+      {...defaultProps}
+      onChange={onChange}
+      resolveThemeTokens={false}
+      outputFormat="hex"
+      presets={[{ label: 'Special Colors', colors: ['Green', 'Red'] }]}
+    />,
+  );
 
-    if (colorPickerTrigger) {
-      await userEvent.click(colorPickerTrigger);
-    }
+  const colorPickerTrigger = document.querySelector(
+    '.ant-color-picker-trigger',
+  );
+  expect(colorPickerTrigger).toBeInTheDocument();
+  await userEvent.click(colorPickerTrigger!);
 
-    // Note: Testing actual color selection in AntD ColorPicker would require more complex mocking
-    // as it uses complex internal components. The main functionality is covered by the component itself.
+  await waitFor(() => {
+    expect(
+      document.querySelector('.ant-color-picker-presets-color'),
+    ).toBeInTheDocument();
   });
 
-  test('includes preset colors from the categorical scheme', () => {
-    render(<ColorPickerControl {...defaultProps} />);
+  const presets = document.querySelectorAll('.ant-color-picker-presets-color');
+  const greenPreset = presets[0];
+
+  expect(greenPreset).toBeInTheDocument();
+  await userEvent.click(greenPreset);
+
+  expect(onChange).toHaveBeenCalledWith('#00960033');
+});
+
+test('resolves colorSuccess theme token correctly when matching color is selected', async () => {
+  const onChange = jest.fn();
 
-    // The component should have access to the preset colors from the registry
-    // This is tested by ensuring the component renders without errors with the presets
-    const colorPickerTrigger = document.querySelector(
-      '.ant-color-picker-trigger',
-    );
-    expect(colorPickerTrigger).toBeInTheDocument();
+  jest
+    .spyOn(require('@apache-superset/core/theme'), 'useTheme')
+    .mockReturnValue({
+      colors: {
+        colorSuccess: 'rgba(82, 196, 26, 1)',
+      },
+    });
+
+  render(
+    <ColorPickerControl
+      {...defaultProps}
+      onChange={onChange}
+      resolveThemeTokens
+      presets={[{ label: 'Theme Tokens', colors: ['colorSuccess'] }]}
+    />,
+  );
+
+  const colorPickerTrigger = document.querySelector(
+    '.ant-color-picker-trigger',
+  );
+  expect(colorPickerTrigger).toBeInTheDocument();
+  await userEvent.click(colorPickerTrigger!);
+
+  await waitFor(() => {
+    expect(
+      document.querySelector('.ant-color-picker-presets-items'),
+    ).toBeInTheDocument();
   });
+
+  const successPreset = document.querySelector(
+    '.ant-color-picker-presets-color [style*="82, 196, 26"]',
+  ) as HTMLElement | null;
+
+  expect(successPreset).toBeInTheDocument();
+
+  await userEvent.click(successPreset!);
+
+  expect(onChange).toHaveBeenCalledWith('colorSuccess');
+});
+
+test('handles theme with nested colors object', () => {
+  jest
+    .spyOn(require('@apache-superset/core/theme'), 'useTheme')
+    .mockReturnValue({
+      colors: { primary: '#007bff' },
+    });
+
+  const { container } = render(<ColorPickerControl {...defaultProps} />);
+  expect(
+    container.querySelector('.ant-color-picker-trigger'),
+  ).toBeInTheDocument();
+});
+
+test('handles theme without colors field', () => {
+  jest
+    .spyOn(require('@apache-superset/core/theme'), 'useTheme')
+    .mockReturnValue({
+      primary: '#007bff',
+    });
+
+  const { container } = render(<ColorPickerControl {...defaultProps} />);
+  expect(
+    container.querySelector('.ant-color-picker-trigger'),
+  ).toBeInTheDocument();
+});
+
+test('handles undefined theme gracefully', () => {
+  jest
+    .spyOn(require('@apache-superset/core/theme'), 'useTheme')
+    .mockReturnValue(undefined);
+
+  expect(() => render(<ColorPickerControl {...defaultProps} />)).not.toThrow();
 });
diff --git a/superset-frontend/src/explore/components/controls/ColorPickerControl.tsx b/superset-frontend/src/explore/components/controls/ColorPickerControl.tsx
index ea16158a5de..cdbb83aeb45 100644
--- a/superset-frontend/src/explore/components/controls/ColorPickerControl.tsx
+++ b/superset-frontend/src/explore/components/controls/ColorPickerControl.tsx
@@ -16,70 +16,215 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { getCategoricalSchemeRegistry } from '@superset-ui/core';
+import { useMemo } from 'react';
+import { getCategoricalSchemeRegistry, rgbaToHex } from '@superset-ui/core';
+import { t } from '@apache-superset/core/translation';
 import {
   ColorPicker,
   type RGBColor,
   type ColorValue,
 } from '@superset-ui/core/components';
 import ControlHeader from '../ControlHeader';
+import { useTheme, type SupersetTheme } from '@apache-superset/core/theme';
+
+const SPECIAL_COLORS = {
+  Red: { r: 150, g: 0, b: 0, a: 0.2 },
+  Green: { r: 0, g: 150, b: 0, a: 0.2 },
+} as const;
+
+type SpecialColorKey = keyof typeof SPECIAL_COLORS;
+export type ColorPickerValue = RGBColor | SpecialColorKey | string;
+export type ColorOutputFormat = 'hex' | 'rgb';
 
 export interface ColorPickerControlProps {
-  onChange?: (color: RGBColor) => void;
-  value?: RGBColor;
+  onChange?: (color: ColorPickerValue) => void;
+  value?: ColorPickerValue;
   name?: string;
   label?: string;
   description?: string;
   renderTrigger?: boolean;
   hovered?: boolean;
   warning?: string;
+  presets?: { label: string; colors: string[] }[];
+  ariaLabel?: string;
+  resolveThemeTokens?: boolean;
+  outputFormat?: ColorOutputFormat;
 }
 
-function rgbToHex(rgb: RGBColor): string {
-  const { r, g, b, a = 1 } = rgb;
-  const toHex = (value: number) => {
-    const hex = Math.round(value).toString(16);
-    return hex.length === 1 ? `0${hex}` : hex;
-  };
+const normalizeColorToHex = (color: string): string => {
+  if (!color) return '';
+
+  if (color.startsWith('#')) {
+    return color.toLowerCase();
+  }
 
-  const hexColor = `#${toHex(r)}${toHex(g)}${toHex(b)}`;
+  const div = document.createElement('div');
+  div.style.color = color;
+  const normalized = div.style.color || '';
 
-  if (a !== undefined && a !== 1) {
-    return `${hexColor}${toHex(Math.round(a * 255))}`;
+  const match = /^rgba?\((\d+),\s+(\d+),\s+(\d+)(?:,\s*([\d.]+))?\)$/.exec(
+    normalized,
+  );
+  if (match) {
+    return rgbaToHex({
+      r: parseInt(match[1], 10),
+      g: parseInt(match[2], 10),
+      b: parseInt(match[3], 10),
+      a: match[4] !== undefined ? parseFloat(match[4]) : 1,
+    }).toLowerCase();
   }
 
-  return hexColor;
+  return color.toLowerCase();
+};
+
+const getReverseThemeColorMap = (
+  themeColors: Record<string, string>,
+): Map<string, string> => {
+  const reverseMap = new Map<string, string>();
+  if (!themeColors) return reverseMap;
+
+  Object.entries(themeColors).forEach(([name, value]) => {
+    if (typeof value === 'string') {
+      const hex = normalizeColorToHex(value);
+      if (!reverseMap.has(hex)) {
+        reverseMap.set(hex, name);
+      }
+    }
+  });
+
+  return reverseMap;
+};
+
+function toDisplayHex(
+  value: ColorPickerValue | undefined,
+  themeColors: Record<string, string>,
+): string | undefined {
+  if (!value) return undefined;
+
+  if (typeof value === 'string') {
+    if (value in SPECIAL_COLORS) {
+      return rgbaToHex(SPECIAL_COLORS[value as SpecialColorKey]).toLowerCase();
+    }
+    if (
+      themeColors &&
+      Object.prototype.hasOwnProperty.call(themeColors, value)
+    ) {
+      return themeColors[value as string].toLowerCase();
+    }
+    return value.toLowerCase();
+  }
+
+  return rgbaToHex(value).toLowerCase();
 }
 
+const extractThemeColors = (
+  theme: SupersetTheme | undefined | null,
+): Record<string, string> => {
+  if (!theme || typeof theme !== 'object') {
+    return {};
+  }
+
+  if (
+    'colors' in theme &&
+    typeof theme.colors === 'object' &&
+    theme.colors !== null
+  ) {
+    return theme.colors as Record<string, string>;
+  }
+
+  return theme as unknown as Record<string, string>;
+};
+
 export default function ColorPickerControl({
   onChange,
   value,
+  presets: customPresets,
+  ariaLabel,
+  resolveThemeTokens = false,
+  outputFormat = 'rgb',
   ...headerProps
 }: ColorPickerControlProps) {
   const categoricalScheme = getCategoricalSchemeRegistry().get();
-  const presetColors = categoricalScheme?.colors.slice(0, 9) || [];
+  const defaultPresets = categoricalScheme?.colors.slice(0, 9) || [];
+  const theme = useTheme();
+
+  const themeColors = useMemo<Record<string, string>>(
+    () => extractThemeColors(theme),
+    [theme],
+  );
+
+  const reverseMap = useMemo(
+    () => getReverseThemeColorMap(themeColors),
+    [themeColors],
+  );
+
+  const presets = useMemo(() => {
+    if (customPresets) {
+      return customPresets.map(item => ({
+        label: item.label,
+        colors: item.colors.map(color => {
+          if (color in SPECIAL_COLORS) {
+            return rgbaToHex(
+              SPECIAL_COLORS[color as SpecialColorKey],
+            ).toLowerCase();
+          }
+          if (
+            themeColors &&
+            Object.prototype.hasOwnProperty.call(themeColors, color as string)
+          ) {
+            return themeColors[color as string].toLowerCase();
+          }
+          return String(color).toLowerCase();
+        }),
+      }));
+    }
+
+    return [
+      {
+        label: t('Theme colors'),
+        colors: defaultPresets.map(c => String(c).toLowerCase()),
+      },
+    ];
+  }, [customPresets, themeColors, defaultPresets]);
 
   const handleChange = (color: ColorValue) => {
-    if (onChange) {
-      const rgb = color.toRgb();
-      onChange({
-        r: rgb.r,
-        g: rgb.g,
-        b: rgb.b,
-        a: rgb.a,
-      });
+    if (!onChange) return;
+
+    const rgb = color.toRgb();
+    const hex = rgbaToHex(rgb).toLowerCase();
+
+    const specialEntry = resolveThemeTokens
+      ? Object.entries(SPECIAL_COLORS).find(
+          ([, rgba]) => rgbaToHex(rgba).toLowerCase() === hex,
+        )
+      : undefined;
+
+    if (specialEntry) {
+      onChange(specialEntry[0] as SpecialColorKey);
+      return;
+    }
+
+    if (resolveThemeTokens && reverseMap.has(hex)) {
+      const tokenName = reverseMap.get(hex);
+      if (tokenName) {
+        onChange(tokenName);
+        return;
+      }
     }
+    if (outputFormat === 'rgb') onChange(rgb);
+    else onChange(hex);
   };
 
-  const hexValue = value ? rgbToHex(value) : undefined;
+  const hexValue = toDisplayHex(value, themeColors);
 
   return (
     <div>
       <ControlHeader {...headerProps} />
       <ColorPicker
+        aria-label={ariaLabel}
         value={hexValue}
         onChangeComplete={handleChange}
-        presets={[{ label: 'Theme colors', colors: presetColors }]}
+        presets={presets}
         showText
       />
     </div>
diff --git a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx
index 7a64b1537d1..774d89e7085 100644
--- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx
+++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx
@@ -21,6 +21,7 @@ import {
   screen,
   fireEvent,
   waitFor,
+  userEvent,
 } from 'spec/helpers/testing-library';
 import { Comparator, ColorSchemeEnum } from '@superset-ui/chart-controls';
 import { GenericDataType } from '@apache-superset/core/common';
@@ -51,12 +52,8 @@ const mixColumns = [
 
 const extraColorChoices = [
   {
-    value: ColorSchemeEnum.Green,
-    label: 'Green for increase, red for decrease',
-  },
-  {
-    value: ColorSchemeEnum.Red,
-    label: 'Red for increase, green for decrease',
+    label: 'Colors',
+    colors: [ColorSchemeEnum.Green, ColorSchemeEnum.Red],
   },
 ];
 
@@ -117,7 +114,7 @@ test('renders the correct input fields based on the selected operator', async ()
 });
 
 test('renders None for operator when Green for increase is selected', async () => {
-  render(
+  const { container } = render(
     <FormattingPopoverContent
       onChange={mockOnChange}
       columns={columns}
@@ -125,15 +122,44 @@ test('renders None for operator when Green for increase is selected', async () =
     />,
   );
 
-  // Select the 'Green for increase' color scheme
-  fireEvent.change(screen.getAllByLabelText(/color scheme/i)[0], {
-    target: { value: ColorSchemeEnum.Green },
+  const colorPickerTrigger = container.querySelector(
+    '.ant-color-picker-trigger',
+  );
+  expect(colorPickerTrigger).toBeInTheDocument();
+  await userEvent.click(colorPickerTrigger!);
+
+  await waitFor(() => {
+    expect(
+      document.querySelector('.ant-color-picker-presets-items'),
+    ).toBeInTheDocument();
+  });
+
+  const presets = document.querySelectorAll('.ant-color-picker-presets-color');
+  const greenPreset = Array.from(presets).find(preset => {
+    const inner = preset.querySelector('.ant-color-picker-color-block-inner');
+    return (
+      inner && inner.getAttribute('style')?.includes('rgba(0, 150, 0, 0.2)')
+    );
   });
 
-  fireEvent.click(await screen.findByTitle(/green for increase/i));
+  expect(greenPreset).toBeDefined();
+  expect(greenPreset).toBeInTheDocument();
+  const safeGreenPreset = greenPreset as HTMLElement;
+
+  const innerColorBlock = safeGreenPreset.querySelector(
+    '.ant-color-picker-color-block-inner',
+  );
+  expect(innerColorBlock).toHaveStyle({ background: 'rgba(0, 150, 0, 0.2)' });
+
+  expect(safeGreenPreset).toBeInTheDocument();
+  await userEvent.click(safeGreenPreset);
+
+  const operatorInput = screen.getByLabelText('Operator');
+  expect(operatorInput).toBeInTheDocument();
 
-  // Assert that the operator is set to 'None'
-  expect(screen.getByText(/none/i)).toBeInTheDocument();
+  const operatorSelect = operatorInput.closest('.ant-select-content');
+  expect(operatorSelect).toBeInTheDocument();
+  expect(operatorSelect).toHaveTextContent(/none/i);
 });
 
 test('displays the correct input fields based on the selected string type operator', async () => {
@@ -295,7 +321,7 @@ test('should hide formatting fields when allColumns is empty', async () => {
 test('should hide formatting fields when color scheme is Green', async () => {
   render(
     <FormattingPopoverContent
-      config={{ colorScheme: extraColorChoices[0].value }}
+      config={{ colorScheme: extraColorChoices[0].colors[0] }}
       columns={mixColumns}
       allColumns={mixColumns}
       onChange={mockOnChange}
@@ -307,3 +333,43 @@ test('should hide formatting fields when color scheme is Green', async () => {
     expect(screen.queryByText('Formatting object')).not.toBeInTheDocument();
   });
 });
+
+test('should not display tooltip when extraColorChoices is not provided', async () => {
+  const { container } = render(
+    <FormattingPopoverContent onChange={mockOnChange} columns={columns} />,
+  );
+
+  const tooltipIcon = container.querySelector('.ant-form-item-tooltip');
+  expect(tooltipIcon).not.toBeInTheDocument();
+});
+
+test('should display tooltip icon when extraColorChoices is provided', () => {
+  const { container } = render(
+    <FormattingPopoverContent
+      onChange={mockOnChange}
+      columns={columns}
+      extraColorChoices={extraColorChoices}
+    />,
+  );
+
+  const tooltipIcon = container.querySelector('.ant-form-item-tooltip');
+  expect(tooltipIcon).toBeInTheDocument();
+
+  const questionIcon = tooltipIcon?.querySelector(
+    '[aria-label="question-circle"]',
+  );
+  expect(questionIcon).toBeInTheDocument();
+});
+
+test('should not display tooltip icon when extraColorChoices is empty', () => {
+  const { container } = render(
+    <FormattingPopoverContent
+      onChange={mockOnChange}
+      columns={columns}
+      extraColorChoices={[]}
+    />,
+  );
+
+  const tooltipIcon = container.querySelector('.ant-form-item-tooltip');
+  expect(tooltipIcon).not.toBeInTheDocument();
+});
diff --git a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx
index 5bda7ba898a..a9bab84e31c 100644
--- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx
+++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx
@@ -44,8 +44,9 @@ import {
   stringOperatorOptions,
   booleanOperatorOptions,
   formattingOptions,
-  colorSchemeOptions,
+  colorScheme,
 } from './constants';
+import ColorPickerControl from '../ColorPickerControl';
 
 const FullWidthInputNumber = styled(InputNumber)`
   width: 100%;
@@ -236,11 +237,11 @@ export const FormattingPopoverContent = ({
   config?: ConditionalFormattingConfig;
   onChange: (config: ConditionalFormattingConfig) => void;
   columns: { label: string; value: string; dataType: GenericDataType }[];
-  extraColorChoices?: { label: string; value: string }[];
+  extraColorChoices?: { label: string; colors: string[] }[];
   allColumns?: ColumnOption[];
 }) => {
   const [form] = Form.useForm();
-  const colorScheme = colorSchemeOptions();
+  const colors = colorScheme();
   const [showOperatorFields, setShowOperatorFields] = useState(
     config === undefined ||
       (config?.colorScheme !== ColorSchemeEnum.Green &&
@@ -319,6 +320,7 @@ export const FormattingPopoverContent = ({
     () => allColumns.filter(col => col.dataType === GenericDataType.Numeric),
     [allColumns],
   );
+  const defaultColorToken = colors[0]?.colors?.[0];
 
   const visibleUseGradient = useMemo(
     () =>
@@ -365,6 +367,14 @@ export const FormattingPopoverContent = ({
     }
   }, [column, columns, previousColumnType]);
 
+  const trendColorsTooltip = (
+    <div>
+      <div>{t('Trend colors are added (for time-based comparison):')}</div>
+      <div>{t('green — increase / red — decrease')}</div>
+      <div>{t('red — increase / green — decrease')}</div>
+    </div>
+  );
+
   return (
     <Form
       form={form}
@@ -395,12 +405,15 @@ export const FormattingPopoverContent = ({
             name="colorScheme"
             label={t('Color scheme')}
             rules={rulesRequired}
-            initialValue={colorScheme[0].value}
+            initialValue={defaultColorToken}
+            tooltip={extraColorChoices.length > 0 ? trendColorsTooltip : ''}
           >
-            <Select
-              onChange={event => handleChange(event)}
+            <ColorPickerControl
               ariaLabel={t('Color scheme')}
-              options={[...colorScheme, ...extraColorChoices]}
+              onChange={event => handleChange(event)}
+              presets={[...colors, ...extraColorChoices]}
+              resolveThemeTokens
+              outputFormat="hex"
             />
           </FormItem>
         </Col>
diff --git a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/constants.ts b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/constants.ts
index 4bc6ce6a76b..ad7b7842d64 100644
--- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/constants.ts
+++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/constants.ts
@@ -65,8 +65,16 @@ export const formattingOptions = [
 ];
 
 // Use theme token names instead of hex values to support theme switching
-export const colorSchemeOptions = () => [
-  { value: 'colorSuccess', label: t('success') },
-  { value: 'colorWarning', label: t('alert') },
-  { value: 'colorError', label: t('error') },
+export const colorScheme = () => [
+  {
+    label: t('Colors'),
+    colors: [
+      'colorSuccess',
+      'colorWarning',
+      'colorError',
+      'colorSuccessBg',
+      'colorWarningBg',
+      'colorErrorBg',
+    ],
+  },
 ];
diff --git a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/types.ts b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/types.ts
index f2a58a162ed..8fcb53296be 100644
--- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/types.ts
+++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/types.ts
@@ -25,6 +25,7 @@ import {
   ObjectFormattingEnum,
 } from '@superset-ui/chart-controls';
 import { GenericDataType } from '@apache-superset/core/common';
+import { type RGBColor } from '@superset-ui/core/components';
 
 export type ConditionalFormattingConfig = {
   operator?: Comparator;
@@ -32,7 +33,7 @@ export type ConditionalFormattingConfig = {
   targetValueLeft?: number;
   targetValueRight?: number;
   column?: string;
-  colorScheme?: string;
+  colorScheme?: RGBColor | string;
   toAllRow?: boolean;
   toTextColor?: boolean;
   useGradient?: boolean;
@@ -48,7 +49,7 @@ export type ConditionalFormattingControlProps = ControlComponentProps<
   verboseMap: Record<string, string>;
   label: string;
   description: string;
-  extraColorChoices?: { label: string; value: string }[];
+  extraColorChoices?: { label: string; colors: string[] }[];
   allColumns?: ColumnOption[];
 };
 
@@ -58,7 +59,7 @@ export type FormattingPopoverProps = PopoverProps & {
   config?: ConditionalFormattingConfig;
   title: string;
   children: ReactNode;
-  extraColorChoices?: { label: string; value: string }[];
+  extraColorChoices?: { label: string; colors: string[] }[];
   allColumns?: ColumnOption[];
 };