diff --git a/frontend/src/metabase/visualizations/components/ChartSettings.tsx b/frontend/src/metabase/visualizations/components/ChartSettings.tsx
deleted file mode 100644
index 8560873b7a12ad7f588fd31b914ffc76d8763c40..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/visualizations/components/ChartSettings.tsx
+++ /dev/null
@@ -1,529 +0,0 @@
-import { assocIn } from "icepick";
-import { Component } from "react";
-import * as React from "react";
-import { t } from "ttag";
-import _ from "underscore";
-
-import Button from "metabase/core/components/Button";
-import Radio from "metabase/core/components/Radio";
-import CS from "metabase/css/core/index.css";
-import {
-  extractRemappings,
-  getVisualizationTransformed,
-} from "metabase/visualizations";
-import Visualization from "metabase/visualizations/components/Visualization";
-import { updateSeriesColor } from "metabase/visualizations/lib/series";
-import {
-  getClickBehaviorSettings,
-  getComputedSettings,
-  getSettingsWidgets,
-  updateSettings,
-} from "metabase/visualizations/lib/settings";
-import { getSettingDefinitionsForColumn } from "metabase/visualizations/lib/settings/column";
-import { keyForSingleSeries } from "metabase/visualizations/lib/settings/series";
-import { getSettingsWidgetsForSeries } from "metabase/visualizations/lib/settings/visualization";
-import type Question from "metabase-lib/v1/Question";
-import { getColumnKey } from "metabase-lib/v1/queries/utils/column-key";
-import type {
-  Dashboard,
-  DashboardCard,
-  DatasetColumn,
-  RawSeries,
-  Series,
-  VisualizationSettings,
-} from "metabase-types/api";
-
-import type { ComputedVisualizationSettings } from "../types";
-
-import {
-  ChartSettingsFooterRoot,
-  ChartSettingsListContainer,
-  ChartSettingsMenu,
-  ChartSettingsPreview,
-  ChartSettingsRoot,
-  ChartSettingsVisualizationContainer,
-  SectionContainer,
-  SectionWarnings,
-} from "./ChartSettings.styled";
-import ChartSettingsWidgetList from "./ChartSettingsWidgetList";
-import ChartSettingsWidgetPopover from "./ChartSettingsWidgetPopover";
-
-// section names are localized
-const DEFAULT_TAB_PRIORITY = [t`Data`];
-
-/**
- * @deprecated HOCs are deprecated
- */
-function withTransientSettingState(
-  ComposedComponent: React.ComponentType<ChartSettingsProps>,
-) {
-  return class extends React.Component<
-    ChartSettingsProps,
-    { settings?: VisualizationSettings }
-  > {
-    static displayName = `withTransientSettingState[${
-      ComposedComponent.displayName || ComposedComponent.name
-    }]`;
-
-    constructor(props: ChartSettingsProps) {
-      super(props);
-      this.state = {
-        settings: props.settings,
-      };
-    }
-
-    UNSAFE_componentWillReceiveProps(nextProps: ChartSettingsProps) {
-      if (this.props.settings !== nextProps.settings) {
-        this.setState({ settings: nextProps.settings });
-      }
-    }
-
-    render() {
-      return (
-        <ComposedComponent
-          {...this.props}
-          settings={this.state.settings}
-          onChange={(settings: VisualizationSettings) =>
-            this.setState({ settings })
-          }
-          onDone={(settings: VisualizationSettings) =>
-            this.props.onChange?.(settings || this.state.settings)
-          }
-        />
-      );
-    }
-  };
-}
-
-// this type is not full, we need to extend it later
-export interface Widget {
-  id: string;
-  section: string;
-  hidden?: boolean;
-  props: Record<string, unknown>;
-  title?: string;
-  widget: (() => JSX.Element | null) | undefined;
-}
-
-export interface ChartSettingsProps {
-  className?: string;
-  dashboard?: Dashboard;
-  dashcard?: DashboardCard;
-  initial?: { section: string; widget?: Widget };
-  onCancel?: () => void;
-  onDone?: (settings: VisualizationSettings) => void;
-  onReset?: () => void;
-  onChange?: (
-    settings: ComputedVisualizationSettings,
-    question?: Question,
-  ) => void;
-  onClose?: () => void;
-  rawSeries?: RawSeries[];
-  settings?: VisualizationSettings;
-  widgets?: Widget[];
-  series: Series;
-  computedSettings?: ComputedVisualizationSettings;
-  isDashboard?: boolean;
-  question?: Question;
-  addField?: () => void;
-  noPreview?: boolean;
-}
-
-interface ChartSettingsState {
-  currentSection: string | null;
-  currentWidget: Widget | null;
-  popoverRef?: HTMLElement | null;
-  warnings?: string[];
-}
-
-class ChartSettings extends Component<ChartSettingsProps, ChartSettingsState> {
-  constructor(props: ChartSettingsProps) {
-    super(props);
-    this.state = {
-      currentSection: (props.initial && props.initial.section) || null,
-      currentWidget: (props.initial && props.initial.widget) || null,
-    };
-  }
-
-  componentDidUpdate(prevProps: ChartSettingsProps) {
-    const { initial } = this.props;
-    if (!_.isEqual(initial, prevProps.initial)) {
-      this.setState({
-        currentSection: (initial && initial.section) || null,
-        currentWidget: (initial && initial.widget) || null,
-      });
-    }
-  }
-
-  handleShowSection = (section: string) => {
-    this.setState({
-      currentSection: section,
-      currentWidget: null,
-    });
-  };
-
-  // allows a widget to temporarily replace itself with a different widget
-  handleShowWidget = (widget: Widget, ref: HTMLElement | null) => {
-    this.setState({ popoverRef: ref, currentWidget: widget });
-  };
-
-  // go back to previously selected section
-  handleEndShowWidget = () => {
-    this.setState({ currentWidget: null, popoverRef: null });
-  };
-
-  handleResetSettings = () => {
-    const originalCardSettings =
-      this.props.dashcard?.card.visualization_settings;
-    const clickBehaviorSettings = getClickBehaviorSettings(this._getSettings());
-
-    this.props.onChange?.({
-      ...originalCardSettings,
-      ...clickBehaviorSettings,
-    });
-  };
-
-  handleChangeSettings = (
-    changedSettings: VisualizationSettings,
-    question: Question,
-  ) => {
-    this.props.onChange?.(
-      updateSettings(this._getSettings(), changedSettings),
-      question,
-    );
-  };
-
-  handleChangeSeriesColor = (seriesKey: string, color: string) => {
-    this.props.onChange?.(
-      updateSeriesColor(this._getSettings(), seriesKey, color),
-    );
-  };
-
-  handleDone = () => {
-    this.props.onDone?.(this._getSettings());
-    this.props.onClose?.();
-  };
-
-  handleCancel = () => {
-    this.props.onClose?.();
-  };
-
-  _getSettings() {
-    return (
-      this.props.settings || this.props.series[0].card.visualization_settings
-    );
-  }
-
-  _getComputedSettings() {
-    return this.props.computedSettings || {};
-  }
-
-  _getWidgets(): Widget[] {
-    if (this.props.widgets) {
-      return this.props.widgets;
-    } else {
-      const { isDashboard, dashboard } = this.props;
-      const transformedSeries = this._getTransformedSeries();
-
-      return getSettingsWidgetsForSeries(
-        transformedSeries,
-        this.handleChangeSettings,
-        isDashboard,
-        { dashboardId: dashboard?.id },
-      );
-    }
-  }
-
-  // TODO: move this logic out of the React component
-  _getRawSeries() {
-    const { series } = this.props;
-    const settings = this._getSettings();
-    const rawSeries = assocIn(
-      series,
-      [0, "card", "visualization_settings"],
-      settings,
-    );
-    return rawSeries;
-  }
-  _getTransformedSeries() {
-    const rawSeries = this._getRawSeries();
-    const { series: transformedSeries } = getVisualizationTransformed(
-      extractRemappings(rawSeries),
-    );
-    return transformedSeries;
-  }
-
-  columnHasSettings(col: DatasetColumn) {
-    const { series } = this.props;
-    const settings = this._getSettings() || {};
-    const settingsDefs = getSettingDefinitionsForColumn(series, col);
-    const computedSettings = getComputedSettings(settingsDefs, col, settings);
-
-    return getSettingsWidgets(
-      settingsDefs,
-      settings,
-      computedSettings,
-      col,
-      _.noop,
-      {
-        series,
-      },
-    ).some(widget => !widget.hidden);
-  }
-
-  getStyleWidget = (widgets: Widget[]): Widget | null => {
-    const series = this._getTransformedSeries();
-    const settings = this._getComputedSettings();
-    const { currentWidget } = this.state;
-    const seriesSettingsWidget =
-      currentWidget && widgets.find(widget => widget.id === "series_settings");
-
-    const display = series?.[0]?.card?.display;
-    // In the pie the chart, clicking on the "measure" settings menu will only
-    // open a formatting widget, and we don't want the style widget (used only
-    // for dimension) to override that
-    if (display === "pie" && currentWidget?.id === "column_settings") {
-      return null;
-    }
-
-    //We don't want to show series settings widget for waterfall charts
-    if (display === "waterfall" || !seriesSettingsWidget) {
-      return null;
-    }
-
-    if (currentWidget.props?.seriesKey !== undefined) {
-      return {
-        ...seriesSettingsWidget,
-        props: {
-          ...seriesSettingsWidget.props,
-          initialKey: currentWidget.props.seriesKey,
-        },
-      };
-    } else if (currentWidget.props?.initialKey) {
-      const hasBreakouts =
-        settings["graph.dimensions"] && settings["graph.dimensions"].length > 1;
-
-      if (hasBreakouts) {
-        return null;
-      }
-
-      const singleSeriesForColumn = series.find(single => {
-        const metricColumn = single.data.cols[1];
-        if (metricColumn) {
-          return (
-            getColumnKey(metricColumn) === currentWidget?.props?.initialKey
-          );
-        }
-      });
-
-      if (singleSeriesForColumn) {
-        return {
-          ...seriesSettingsWidget,
-          props: {
-            ...seriesSettingsWidget.props,
-            initialKey: keyForSingleSeries(singleSeriesForColumn),
-          },
-        };
-      }
-    }
-
-    return null;
-  };
-
-  getFormattingWidget = (widgets: Widget[]): Widget | null => {
-    const { currentWidget } = this.state;
-    const widget =
-      currentWidget && widgets.find(widget => widget.id === currentWidget.id);
-
-    if (widget) {
-      return { ...widget, props: { ...widget.props, ...currentWidget.props } };
-    }
-
-    return null;
-  };
-
-  render() {
-    const {
-      className,
-      question,
-      addField,
-      noPreview = false,
-      dashboard,
-      dashcard,
-      isDashboard = false,
-    } = this.props;
-    const { popoverRef } = this.state;
-
-    const settings = this._getSettings();
-    const widgets = this._getWidgets();
-    const rawSeries = this._getRawSeries();
-
-    const widgetsById: Record<string, Widget> = {};
-    const sections: Record<string, Widget[]> = {};
-
-    for (const widget of widgets) {
-      widgetsById[widget.id] = widget;
-      if (widget.widget && !widget.hidden) {
-        sections[widget.section] = sections[widget.section] || [];
-        sections[widget.section].push(widget);
-      }
-    }
-
-    // Move settings from the "undefined" section in the first tab
-    if (sections["undefined"] && Object.values(sections).length > 1) {
-      const extra = sections["undefined"];
-      delete sections["undefined"];
-      Object.values(sections)[0].unshift(...extra);
-    }
-
-    const sectionNames = Object.keys(sections);
-
-    // This sorts the section radio buttons.
-    const sectionSortOrder = [
-      "data",
-      "display",
-      "axes",
-      // include all section names so any forgotten sections are sorted to the end
-      ...sectionNames.map(x => x.toLowerCase()),
-    ];
-    sectionNames.sort((a, b) => {
-      const [aIdx, bIdx] = [a, b].map(x =>
-        sectionSortOrder.indexOf(x.toLowerCase()),
-      );
-      return aIdx - bIdx;
-    });
-
-    const currentSection =
-      this.state.currentSection && sections[this.state.currentSection]
-        ? this.state.currentSection
-        : _.find(DEFAULT_TAB_PRIORITY, name => name in sections) ||
-          sectionNames[0];
-
-    const visibleWidgets = sections[currentSection] || [];
-
-    // This checks whether the current section contains a column settings widget
-    // at the top level. If it does, we avoid hiding the section tabs and
-    // overriding the sidebar title.
-    const currentSectionHasColumnSettings = (
-      sections[currentSection] || []
-    ).some((widget: Widget) => widget.id === "column_settings");
-
-    const extraWidgetProps = {
-      // NOTE: special props to support adding additional fields
-      question: question,
-      addField: addField,
-      onShowWidget: this.handleShowWidget,
-      onEndShowWidget: this.handleEndShowWidget,
-      currentSectionHasColumnSettings,
-      columnHasSettings: (col: DatasetColumn) => this.columnHasSettings(col),
-      onChangeSeriesColor: (seriesKey: string, color: string) =>
-        this.handleChangeSeriesColor(seriesKey, color),
-    };
-
-    const sectionPicker = (
-      <SectionContainer isDashboard={isDashboard}>
-        <Radio
-          value={currentSection}
-          onChange={this.handleShowSection}
-          options={sectionNames}
-          optionNameFn={v => v}
-          optionValueFn={v => v}
-          optionKeyFn={v => v}
-          variant="underlined"
-        />
-      </SectionContainer>
-    );
-
-    const onReset =
-      !_.isEqual(settings, {}) && (settings || {}).virtual_card == null // resetting virtual cards wipes the text and broke the UI (metabase#14644)
-        ? this.handleResetSettings
-        : null;
-
-    const showSectionPicker =
-      // don't show section tabs for a single section
-      sectionNames.length > 1 &&
-      // hide the section picker if the only widget is column_settings
-      !(
-        visibleWidgets.length === 1 &&
-        visibleWidgets[0].id === "column_settings" &&
-        // and this section doesn't doesn't have that as a direct child
-        !currentSectionHasColumnSettings
-      );
-
-    // default layout with visualization
-    return (
-      <ChartSettingsRoot className={className}>
-        <ChartSettingsMenu data-testid="chartsettings-sidebar">
-          {showSectionPicker && sectionPicker}
-          <ChartSettingsListContainer className={CS.scrollShow}>
-            <ChartSettingsWidgetList
-              widgets={visibleWidgets}
-              extraWidgetProps={extraWidgetProps}
-            />
-          </ChartSettingsListContainer>
-        </ChartSettingsMenu>
-        {!noPreview && (
-          <ChartSettingsPreview>
-            <SectionWarnings warnings={this.state.warnings} size={20} />
-            <ChartSettingsVisualizationContainer>
-              <Visualization
-                className={CS.spread}
-                rawSeries={rawSeries}
-                showTitle
-                isEditing
-                isDashboard
-                dashboard={dashboard}
-                dashcard={dashcard}
-                isSettings
-                showWarnings
-                onUpdateVisualizationSettings={this.handleChangeSettings}
-                onUpdateWarnings={(warnings: string[]) =>
-                  this.setState({ warnings })
-                }
-              />
-            </ChartSettingsVisualizationContainer>
-            <ChartSettingsFooter
-              onDone={this.handleDone}
-              onCancel={this.handleCancel}
-              onReset={onReset}
-            />
-          </ChartSettingsPreview>
-        )}
-        <ChartSettingsWidgetPopover
-          anchor={popoverRef as HTMLElement}
-          widgets={[
-            this.getStyleWidget(widgets),
-            this.getFormattingWidget(widgets),
-          ].filter((widget): widget is Widget => !!widget)}
-          handleEndShowWidget={this.handleEndShowWidget}
-        />
-      </ChartSettingsRoot>
-    );
-  }
-}
-
-const ChartSettingsFooter = ({
-  onDone,
-  onCancel,
-  onReset,
-}: {
-  onDone: () => void;
-  onCancel: () => void;
-  onReset: (() => void) | null;
-}) => (
-  <ChartSettingsFooterRoot>
-    {onReset && (
-      <Button
-        borderless
-        icon="refresh"
-        onClick={onReset}
-      >{t`Reset to defaults`}</Button>
-    )}
-    <Button onClick={onCancel}>{t`Cancel`}</Button>
-    <Button primary onClick={onDone}>{t`Done`}</Button>
-  </ChartSettingsFooterRoot>
-);
-
-export { ChartSettings };
-
-export const ChartSettingsWithState = withTransientSettingState(ChartSettings);
diff --git a/frontend/src/metabase/visualizations/components/ChartSettings.styled.tsx b/frontend/src/metabase/visualizations/components/ChartSettings/ChartSettings.styled.tsx
similarity index 100%
rename from frontend/src/metabase/visualizations/components/ChartSettings.styled.tsx
rename to frontend/src/metabase/visualizations/components/ChartSettings/ChartSettings.styled.tsx
diff --git a/frontend/src/metabase/visualizations/components/ChartSettings/ChartSettings.tsx b/frontend/src/metabase/visualizations/components/ChartSettings/ChartSettings.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7c62b16fd0161a569ab6db9a5bbda0c56ae8dd16
--- /dev/null
+++ b/frontend/src/metabase/visualizations/components/ChartSettings/ChartSettings.tsx
@@ -0,0 +1,415 @@
+import { assocIn } from "icepick";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { t } from "ttag";
+import _ from "underscore";
+
+import Radio from "metabase/core/components/Radio";
+import CS from "metabase/css/core/index.css";
+import {
+  extractRemappings,
+  getVisualizationTransformed,
+} from "metabase/visualizations";
+import { ChartSettingsFooter } from "metabase/visualizations/components/ChartSettings/ChartSettingsFooter";
+import Visualization from "metabase/visualizations/components/Visualization";
+import { updateSeriesColor } from "metabase/visualizations/lib/series";
+import {
+  getClickBehaviorSettings,
+  getComputedSettings,
+  getSettingsWidgets,
+  updateSettings,
+} from "metabase/visualizations/lib/settings";
+import { getSettingDefinitionsForColumn } from "metabase/visualizations/lib/settings/column";
+import { keyForSingleSeries } from "metabase/visualizations/lib/settings/series";
+import { getSettingsWidgetsForSeries } from "metabase/visualizations/lib/settings/visualization";
+import type Question from "metabase-lib/v1/Question";
+import { getColumnKey } from "metabase-lib/v1/queries/utils/column-key";
+import type { DatasetColumn, VisualizationSettings } from "metabase-types/api";
+
+import ChartSettingsWidgetList from "../ChartSettingsWidgetList";
+import ChartSettingsWidgetPopover from "../ChartSettingsWidgetPopover";
+
+import {
+  ChartSettingsListContainer,
+  ChartSettingsMenu,
+  ChartSettingsPreview,
+  ChartSettingsRoot,
+  ChartSettingsVisualizationContainer,
+  SectionContainer,
+  SectionWarnings,
+} from "./ChartSettings.styled";
+import type {
+  ChartSettingsProps,
+  ChartSettingsWithStateProps,
+  Widget,
+} from "./types";
+
+// section names are localized
+const DEFAULT_TAB_PRIORITY = [t`Data`];
+
+export const ChartSettings = ({
+  initial,
+  settings: propSettings,
+  series,
+  computedSettings: propComputedSettings,
+  onChange,
+  isDashboard = false,
+  noPreview = false,
+  dashboard,
+  dashcard,
+  onDone,
+  onClose,
+  question,
+  className,
+  widgets: propWidgets,
+}: ChartSettingsProps) => {
+  const [currentSection, setCurrentSection] = useState<string | null>(
+    initial?.section ?? null,
+  );
+  const [currentWidget, setCurrentWidget] = useState<Widget | null>(
+    initial?.widget ?? null,
+  );
+  const [popoverRef, setPopoverRef] = useState<HTMLElement | null>();
+  const [warnings, setWarnings] = useState();
+
+  const chartSettings = useMemo(
+    () => propSettings || series[0].card.visualization_settings,
+    [series, propSettings],
+  );
+
+  const computedSettings = useMemo(
+    () => propComputedSettings || {},
+    [propComputedSettings],
+  );
+
+  const handleChangeSettings = useCallback(
+    (changedSettings: VisualizationSettings, question: Question) => {
+      onChange?.(updateSettings(chartSettings, changedSettings), question);
+    },
+    [chartSettings, onChange],
+  );
+
+  const chartSettingsRawSeries = useMemo(
+    () => assocIn(series, [0, "card", "visualization_settings"], chartSettings),
+    [chartSettings, series],
+  );
+
+  const transformedSeries = useMemo(() => {
+    const { series: transformedSeries } = getVisualizationTransformed(
+      extractRemappings(chartSettingsRawSeries),
+    );
+    return transformedSeries;
+  }, [chartSettingsRawSeries]);
+
+  const widgets = useMemo(
+    () =>
+      propWidgets ||
+      getSettingsWidgetsForSeries(
+        transformedSeries,
+        handleChangeSettings,
+        isDashboard,
+        { dashboardId: dashboard?.id },
+      ),
+    [
+      propWidgets,
+      transformedSeries,
+      handleChangeSettings,
+      isDashboard,
+      dashboard?.id,
+    ],
+  );
+
+  const columnHasSettings = useCallback(
+    (col: DatasetColumn) => {
+      const settings = chartSettings || {};
+      const settingsDefs = getSettingDefinitionsForColumn(series, col);
+      const getComputedSettingsResult = getComputedSettings(
+        settingsDefs,
+        col,
+        settings,
+      );
+
+      return getSettingsWidgets(
+        settingsDefs,
+        settings,
+        getComputedSettingsResult,
+        col,
+        _.noop,
+        {
+          series: series,
+        },
+      ).some(widget => !widget.hidden);
+    },
+    [chartSettings, series],
+  );
+
+  const styleWidget = useMemo(() => {
+    const seriesSettingsWidget =
+      currentWidget && widgets.find(widget => widget.id === "series_settings");
+
+    const display = transformedSeries?.[0]?.card?.display;
+    // In the pie the chart, clicking on the "measure" settings menu will only
+    // open a formatting widget, and we don't want the style widget (used only
+    // for dimension) to override that
+    if (display === "pie" && currentWidget?.id === "column_settings") {
+      return null;
+    }
+
+    //We don't want to show series settings widget for waterfall charts
+    if (display === "waterfall" || !seriesSettingsWidget) {
+      return null;
+    }
+
+    if (currentWidget.props?.seriesKey !== undefined) {
+      return {
+        ...seriesSettingsWidget,
+        props: {
+          ...seriesSettingsWidget.props,
+          initialKey: currentWidget.props.seriesKey,
+        },
+      };
+    } else if (currentWidget.props?.initialKey) {
+      const hasBreakouts =
+        computedSettings["graph.dimensions"] &&
+        computedSettings["graph.dimensions"].length > 1;
+
+      if (hasBreakouts) {
+        return null;
+      }
+
+      const singleSeriesForColumn = transformedSeries.find(single => {
+        const metricColumn = single.data.cols[1];
+        if (metricColumn) {
+          return (
+            getColumnKey(metricColumn) === currentWidget?.props?.initialKey
+          );
+        }
+      });
+
+      if (singleSeriesForColumn) {
+        return {
+          ...seriesSettingsWidget,
+          props: {
+            ...seriesSettingsWidget.props,
+            initialKey: keyForSingleSeries(singleSeriesForColumn),
+          },
+        };
+      }
+    }
+
+    return null;
+  }, [computedSettings, currentWidget, transformedSeries, widgets]);
+
+  const formattingWidget = useMemo(() => {
+    const widget =
+      currentWidget && widgets.find(widget => widget.id === currentWidget.id);
+
+    if (widget) {
+      return { ...widget, props: { ...widget.props, ...currentWidget.props } };
+    }
+
+    return null;
+  }, [currentWidget, widgets]);
+
+  const handleShowSection = useCallback((section: string) => {
+    setCurrentSection(section);
+    setCurrentWidget(null);
+  }, []);
+
+  // allows a widget to temporarily replace itself with a different widget
+  const handleShowWidget = useCallback(
+    (widget: Widget, ref: HTMLElement | null) => {
+      setPopoverRef(ref);
+      setCurrentWidget(widget);
+    },
+    [],
+  );
+
+  // go back to previously selected section
+  const handleEndShowWidget = useCallback(() => {
+    setPopoverRef(null);
+    setCurrentWidget(null);
+  }, []);
+
+  const handleResetSettings = useCallback(() => {
+    const originalCardSettings = dashcard?.card.visualization_settings;
+    const clickBehaviorSettings = getClickBehaviorSettings(chartSettings);
+
+    onChange?.({
+      ...originalCardSettings,
+      ...clickBehaviorSettings,
+    });
+  }, [chartSettings, dashcard?.card.visualization_settings, onChange]);
+
+  const handleChangeSeriesColor = useCallback(
+    (seriesKey: string, color: string) => {
+      onChange?.(updateSeriesColor(chartSettings, seriesKey, color));
+    },
+    [chartSettings, onChange],
+  );
+
+  const handleDone = useCallback(() => {
+    onDone?.(chartSettings);
+    onClose?.();
+  }, [chartSettings, onClose, onDone]);
+
+  const handleCancel = useCallback(() => {
+    onClose?.();
+  }, [onClose]);
+
+  const sections: Record<string, Widget[]> = useMemo(() => {
+    const sectionObj: Record<string, Widget[]> = {};
+    for (const widget of widgets) {
+      if (widget.widget && !widget.hidden) {
+        sectionObj[widget.section] = sectionObj[widget.section] || [];
+        sectionObj[widget.section].push(widget);
+      }
+    }
+
+    // Move settings from the "undefined" section in the first tab
+    if (sectionObj["undefined"] && Object.values(sectionObj).length > 1) {
+      const extra = sectionObj["undefined"];
+      delete sectionObj["undefined"];
+      Object.values(sectionObj)[0].unshift(...extra);
+    }
+    return sectionObj;
+  }, [widgets]);
+
+  const sectionNames = Object.keys(sections);
+
+  // This sorts the section radio buttons.
+  const sectionSortOrder = [
+    "data",
+    "display",
+    "axes",
+    // include all section names so any forgotten sections are sorted to the end
+    ...sectionNames.map(x => x.toLowerCase()),
+  ];
+  sectionNames.sort((a, b) => {
+    const [aIdx, bIdx] = [a, b].map(x =>
+      sectionSortOrder.indexOf(x.toLowerCase()),
+    );
+    return aIdx - bIdx;
+  });
+
+  const chartSettingCurrentSection = useMemo(
+    () =>
+      currentSection && sections[currentSection]
+        ? currentSection
+        : _.find(DEFAULT_TAB_PRIORITY, name => name in sections) ||
+          sectionNames[0],
+    [currentSection, sectionNames, sections],
+  );
+
+  const visibleWidgets = sections[chartSettingCurrentSection] || [];
+
+  const currentSectionHasColumnSettings = (
+    sections[chartSettingCurrentSection] || []
+  ).some((widget: Widget) => widget.id === "column_settings");
+
+  const extraWidgetProps = {
+    // NOTE: special props to support adding additional fields
+    question,
+    onShowWidget: handleShowWidget,
+    onEndShowWidget: handleEndShowWidget,
+    currentSectionHasColumnSettings,
+    columnHasSettings,
+    onChangeSeriesColor: handleChangeSeriesColor,
+  };
+
+  const onResetToDefault =
+    // resetting virtual cards wipes the text and broke the UI (metabase#14644)
+    !_.isEqual(chartSettings, {}) && (chartSettings || {}).virtual_card == null
+      ? handleResetSettings
+      : null;
+
+  const showSectionPicker =
+    // don't show section tabs for a single section
+    sectionNames.length > 1 &&
+    // hide the section picker if the only widget is column_settings
+    !(
+      visibleWidgets.length === 1 &&
+      visibleWidgets[0].id === "column_settings" &&
+      // and this section doesn't have that as a direct child
+      !currentSectionHasColumnSettings
+    );
+
+  return (
+    <ChartSettingsRoot className={className}>
+      <ChartSettingsMenu data-testid="chartsettings-sidebar">
+        {showSectionPicker && (
+          <SectionContainer isDashboard={false}>
+            <Radio
+              value={chartSettingCurrentSection ?? undefined}
+              onChange={handleShowSection}
+              options={sectionNames}
+              optionNameFn={v => v}
+              optionValueFn={v => v}
+              optionKeyFn={v => v}
+              variant="underlined"
+            />
+          </SectionContainer>
+        )}
+        <ChartSettingsListContainer className={CS.scrollShow}>
+          <ChartSettingsWidgetList
+            widgets={visibleWidgets}
+            extraWidgetProps={extraWidgetProps}
+          />
+        </ChartSettingsListContainer>
+      </ChartSettingsMenu>
+      {!noPreview && (
+        <ChartSettingsPreview>
+          <SectionWarnings warnings={warnings} size={20} />
+          <ChartSettingsVisualizationContainer>
+            <Visualization
+              className={CS.spread}
+              rawSeries={chartSettingsRawSeries}
+              showTitle
+              isEditing
+              isDashboard
+              dashboard={dashboard}
+              dashcard={dashcard}
+              isSettings
+              showWarnings
+              onUpdateVisualizationSettings={handleChangeSettings}
+              onUpdateWarnings={setWarnings}
+            />
+          </ChartSettingsVisualizationContainer>
+          <ChartSettingsFooter
+            onDone={handleDone}
+            onCancel={handleCancel}
+            onReset={onResetToDefault}
+          />
+        </ChartSettingsPreview>
+      )}
+      <ChartSettingsWidgetPopover
+        anchor={popoverRef as HTMLElement}
+        widgets={[styleWidget, formattingWidget].filter(
+          (widget): widget is Widget => !!widget,
+        )}
+        handleEndShowWidget={handleEndShowWidget}
+      />
+    </ChartSettingsRoot>
+  );
+};
+
+export const ChartSettingsWithState = (props: ChartSettingsWithStateProps) => {
+  const [tempSettings, setTempSettings] = useState(props.settings);
+
+  useEffect(() => {
+    if (props.settings) {
+      setTempSettings(props.settings);
+    }
+  }, [props.settings]);
+
+  const onDone = (settings: VisualizationSettings) =>
+    props.onChange?.(settings ?? tempSettings);
+
+  return (
+    <ChartSettings
+      {...props}
+      onChange={setTempSettings}
+      onDone={onDone}
+      settings={tempSettings}
+    />
+  );
+};
diff --git a/frontend/src/metabase/visualizations/components/ChartSettings.unit.spec.tsx b/frontend/src/metabase/visualizations/components/ChartSettings/ChartSettings.unit.spec.tsx
similarity index 97%
rename from frontend/src/metabase/visualizations/components/ChartSettings.unit.spec.tsx
rename to frontend/src/metabase/visualizations/components/ChartSettings/ChartSettings.unit.spec.tsx
index 9703c7d48efa8feadd8ec35758bc07636c7a89db..583c395ed5d15aae2141ac7af1a32e1b612a2c18 100644
--- a/frontend/src/metabase/visualizations/components/ChartSettings.unit.spec.tsx
+++ b/frontend/src/metabase/visualizations/components/ChartSettings/ChartSettings.unit.spec.tsx
@@ -1,11 +1,6 @@
 import userEvent from "@testing-library/user-event";
 
 import { fireEvent, renderWithProviders, screen } from "__support__/ui";
-import {
-  ChartSettings,
-  type ChartSettingsProps,
-  type Widget,
-} from "metabase/visualizations/components/ChartSettings";
 import registerVisualizations from "metabase/visualizations/register";
 import {
   createMockCard,
@@ -14,6 +9,9 @@ import {
   createMockVisualizationSettings,
 } from "metabase-types/api/mocks";
 
+import { ChartSettings } from "./ChartSettings";
+import type { ChartSettingsProps, Widget } from "./types";
+
 registerVisualizations();
 
 const DEFAULT_PROPS = {
diff --git a/frontend/src/metabase/visualizations/components/ChartSettings/ChartSettingsFooter.tsx b/frontend/src/metabase/visualizations/components/ChartSettings/ChartSettingsFooter.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..19a76b704762ae6072d4983df91f775cc1fa51ac
--- /dev/null
+++ b/frontend/src/metabase/visualizations/components/ChartSettings/ChartSettingsFooter.tsx
@@ -0,0 +1,29 @@
+import { t } from "ttag";
+
+import Button from "metabase/core/components/Button";
+
+import { ChartSettingsFooterRoot } from "./ChartSettings.styled";
+
+type ChartSettingsFooterProps = {
+  onDone: () => void;
+  onCancel: () => void;
+  onReset: (() => void) | null;
+};
+
+export const ChartSettingsFooter = ({
+  onDone,
+  onCancel,
+  onReset,
+}: ChartSettingsFooterProps) => (
+  <ChartSettingsFooterRoot>
+    {onReset && (
+      <Button
+        borderless
+        icon="refresh"
+        onClick={onReset}
+      >{t`Reset to defaults`}</Button>
+    )}
+    <Button onClick={onCancel}>{t`Cancel`}</Button>
+    <Button primary onClick={onDone}>{t`Done`}</Button>
+  </ChartSettingsFooterRoot>
+);
diff --git a/frontend/src/metabase/visualizations/components/ChartSettings/index.ts b/frontend/src/metabase/visualizations/components/ChartSettings/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..50271a0d71d347e1353cfdfe1439e2ad69636c70
--- /dev/null
+++ b/frontend/src/metabase/visualizations/components/ChartSettings/index.ts
@@ -0,0 +1 @@
+export * from "./ChartSettings";
diff --git a/frontend/src/metabase/visualizations/components/ChartSettings/types.ts b/frontend/src/metabase/visualizations/components/ChartSettings/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d3db2bfe55fd9693a88ba87c94df11ad444a4cf1
--- /dev/null
+++ b/frontend/src/metabase/visualizations/components/ChartSettings/types.ts
@@ -0,0 +1,45 @@
+import type { ComputedVisualizationSettings } from "metabase/visualizations/types";
+import type Question from "metabase-lib/v1/Question";
+import type {
+  Dashboard,
+  DashboardCard,
+  Series,
+  VisualizationSettings,
+} from "metabase-types/api";
+
+// this type is not full, we need to extend it later
+export type Widget = {
+  id: string;
+  section: string;
+  hidden?: boolean;
+  props: Record<string, unknown>;
+  title?: string;
+  widget: (() => JSX.Element | null) | undefined;
+};
+
+export type ChartSettingsWithStateProps = {
+  className?: string;
+  isDashboard?: boolean;
+  dashboard?: Dashboard;
+  dashcard?: DashboardCard;
+  initial?: {
+    section: string;
+    widget?: Widget;
+  };
+  onClose?: () => void;
+  series: Series;
+  computedSettings?: ComputedVisualizationSettings;
+  question?: Question;
+  noPreview?: boolean;
+  widgets?: Widget[];
+
+  onChange?: (
+    settings: ComputedVisualizationSettings,
+    question?: Question,
+  ) => void;
+  settings?: VisualizationSettings;
+};
+
+export type ChartSettingsProps = ChartSettingsWithStateProps & {
+  onDone?: (settings: VisualizationSettings) => void;
+};