diff --git a/frontend/src/metabase/components/Select.info.js b/frontend/src/metabase/components/Select.info.js
index 8abe0ed4a7b981c2af2f0736e3ab37e87a893d71..280ae579959117522f778e13802e51b15256c8ea 100644
--- a/frontend/src/metabase/components/Select.info.js
+++ b/frontend/src/metabase/components/Select.info.js
@@ -5,10 +5,10 @@ import Select, { Option } from "metabase/components/Select";
 export const component = Select;
 
 const fixture = [
-  { name: t`Blue` },
-  { name: t`Green` },
-  { name: t`Red` },
-  { name: t`Yellow` },
+  { name: t`Blue`, value: "blue" },
+  { name: t`Green`, value: "green" },
+  { name: t`Red`, value: "red" },
+  { name: t`Yellow`, value: "yellow" },
 ];
 
 export const description = t`
@@ -17,12 +17,16 @@ export const description = t`
 
 export const examples = {
   Default: (
-    <Select onChange={() => alert(t`Selected`)}>
+    <Select value="yellow" onChange={() => alert(t`Selected`)}>
       {fixture.map(f => <Option name={f.name}>{f.name}</Option>)}
     </Select>
   ),
   "With search": (
-    <Select searchProp="name" onChange={() => alert(t`Selected`)}>
+    <Select
+      value="yellow"
+      searchProp="name"
+      onChange={() => alert(t`Selected`)}
+    >
       {fixture.map(f => <Option name={f.name}>{f.name}</Option>)}
     </Select>
   ),
diff --git a/frontend/src/metabase/components/Select.jsx b/frontend/src/metabase/components/Select.jsx
index bf8a4dfcbdcbb20887357bcb911dc418b9434886..037164d5df09ac6e20d3ca99e2331679f56f153c 100644
--- a/frontend/src/metabase/components/Select.jsx
+++ b/frontend/src/metabase/components/Select.jsx
@@ -10,6 +10,7 @@ import Icon from "metabase/components/Icon.jsx";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
 
 import cx from "classnames";
+import _ from "underscore";
 
 export default class Select extends Component {
   static propTypes = {
@@ -48,21 +49,27 @@ class BrowserSelect extends Component {
     // we should not allow this
     className: PropTypes.string,
     compact: PropTypes.bool,
+    multiple: PropTypes.bool,
   };
   static defaultProps = {
     className: "",
     width: 320,
     height: 320,
     rowHeight: 40,
+    multiple: false,
   };
 
   isSelected(otherValue) {
-    const { value } = this.props;
-    return (
-      value === otherValue ||
-      ((value == null || value === "") &&
-        (otherValue == null || otherValue === ""))
-    );
+    const { value, multiple } = this.props;
+    if (multiple) {
+      return _.any(value, v => v === otherValue);
+    } else {
+      return (
+        value === otherValue ||
+        ((value == null || value === "") &&
+          (otherValue == null || otherValue === ""))
+      );
+    }
   }
 
   render() {
@@ -78,18 +85,16 @@ class BrowserSelect extends Component {
       width,
       height,
       rowHeight,
+      multiple,
     } = this.props;
 
     let children = this.props.children;
 
-    let selectedName;
-    for (const child of children) {
-      if (this.isSelected(child.props.value)) {
-        selectedName = child.props.children;
-      }
-    }
-    if (selectedName == null && placeholder) {
-      selectedName = placeholder;
+    let selectedNames = children
+      .filter(child => this.isSelected(child.props.value))
+      .map(child => child.props.children);
+    if (_.isEmpty(selectedNames) && placeholder) {
+      selectedNames = [placeholder];
     }
 
     const { inputValue } = this.state;
@@ -128,7 +133,14 @@ class BrowserSelect extends Component {
         className={className}
         triggerElement={
           triggerElement || (
-            <SelectButton hasValue={!!value}>{selectedName}</SelectButton>
+            <SelectButton hasValue={multiple ? value.length > 0 : !!value}>
+              {selectedNames.map((name, index) => (
+                <span key={index}>
+                  {name}
+                  {index < selectedNames.length - 1 ? ", " : ""}
+                </span>
+              ))}
+            </SelectButton>
           )
         }
         triggerClasses={className}
@@ -171,9 +183,18 @@ class BrowserSelect extends Component {
                     selected: this.isSelected(child.props.value),
                     onClick: () => {
                       if (!child.props.disabled) {
-                        onChange({ target: { value: child.props.value } });
+                        if (multiple) {
+                          const value = this.isSelected(child.props.value)
+                            ? this.props.value.filter(
+                                v => v !== child.props.value,
+                              )
+                            : this.props.value.concat([child.props.value]);
+                          onChange({ target: { value } });
+                        } else {
+                          onChange({ target: { value: child.props.value } });
+                          this.refs.popover.close();
+                        }
                       }
-                      this.refs.popover.close();
                     },
                   })}
                 </div>
diff --git a/frontend/src/metabase/components/Triggerable.jsx b/frontend/src/metabase/components/Triggerable.jsx
index d4e0a9a726559221d066af5fd870509b54021e66..2d4b9342e179e783f45558a1bb18aaa0847b77ef 100644
--- a/frontend/src/metabase/components/Triggerable.jsx
+++ b/frontend/src/metabase/components/Triggerable.jsx
@@ -118,13 +118,16 @@ export default ComposedComponent =>
         });
       }
 
-      // if we have a single child which isn't an HTML element and doesn't have an onClose prop go ahead and inject it directly
       let { children } = this.props;
-      if (
+      if (typeof children === "function") {
+        // if children is a render prop, pass onClose to it
+        children = children({ onClose: this.onClose });
+      } else if (
         React.Children.count(children) === 1 &&
         React.Children.only(children).props.onClose === undefined &&
         typeof React.Children.only(children).type !== "string"
       ) {
+        // if we have a single child which isn't an HTML element and doesn't have an onClose prop go ahead and inject it directly
         children = React.cloneElement(children, { onClose: this.onClose });
       }
 
diff --git a/frontend/src/metabase/css/core/bordered.css b/frontend/src/metabase/css/core/bordered.css
index fa653208337eee4e75c861ffdf9422a24c74b455..4cc9ca9b4fa6876be8d851e9162d9b85db6e6c91 100644
--- a/frontend/src/metabase/css/core/bordered.css
+++ b/frontend/src/metabase/css/core/bordered.css
@@ -16,7 +16,7 @@
 }
 
 /* ensure that a border-top item inside of a bordred element won't double up */
-.bordered .border-bottom:last-child {
+.bordered > .border-bottom:last-child {
   border-bottom: none;
 }
 
@@ -26,7 +26,7 @@
 }
 
 /* ensure that a border-top item inside of a bordred element won't double up */
-.bordered .border-top:first-child {
+.bordered > .border-top:first-child {
   border-top: none;
 }
 
@@ -96,6 +96,10 @@
   border-color: var(--brand-color) !important;
 }
 
+.border-transparent {
+  border-color: transparent;
+}
+
 .border-brand-hover:hover {
   border-color: var(--brand-color);
 }
diff --git a/frontend/src/metabase/lib/colors.js b/frontend/src/metabase/lib/colors.js
index 126cb086e53a0d346621b3f7c868e74488d75067..93807810d9fb93798c78eb42f41f5c3c21112ea9 100644
--- a/frontend/src/metabase/lib/colors.js
+++ b/frontend/src/metabase/lib/colors.js
@@ -1,5 +1,7 @@
 // @flow
 
+import d3 from "d3";
+
 type ColorName = string;
 type Color = string;
 type ColorFamily = { [name: ColorName]: Color };
@@ -73,3 +75,20 @@ export const getRandomColor = (family: ColorFamily): Color => {
   const colors: Color[] = Object.values(family);
   return colors[Math.floor(Math.random() * colors.length)];
 };
+
+type ColorScale = (input: number) => Color;
+
+export const getColorScale = (
+  extent: [number, number],
+  colors: string[],
+): ColorScale => {
+  const [start, end] = extent;
+  return d3.scale
+    .linear()
+    .domain(
+      colors.length === 3
+        ? [start, start + (end - start) / 2, end]
+        : [start, end],
+    )
+    .range(colors);
+};
diff --git a/frontend/src/metabase/lib/data_grid.js b/frontend/src/metabase/lib/data_grid.js
index 46f900291c9ef94997f48a2b38fd0f3bbd67f165..440904298822eedae2caf34a3aa922cf8d055754 100644
--- a/frontend/src/metabase/lib/data_grid.js
+++ b/frontend/src/metabase/lib/data_grid.js
@@ -1,5 +1,3 @@
-import _ from "underscore";
-
 import * as SchemaMetadata from "metabase/lib/schema_metadata";
 import { formatValue } from "metabase/lib/formatting";
 
@@ -66,18 +64,19 @@ export function pivot(data) {
     if (idx === 0) {
       // first column is always the coldef of the normal column
       return data.cols[normalCol];
+    } else {
+      return {
+        ...data.cols[cellCol],
+        // `name` must be the same for conditional formatting, but put the
+        // formatted pivotted value in the `display_name`
+        display_name: formatValue(value, { column: data.cols[pivotCol] }) || "",
+        // for onVisualizationClick:
+        _dimension: {
+          value: value,
+          column: data.cols[pivotCol],
+        },
+      };
     }
-
-    let colDef = _.clone(data.cols[cellCol]);
-    colDef.name = colDef.display_name =
-      formatValue(value, { column: data.cols[pivotCol] }) || "";
-    // for onVisualizationClick:
-    colDef._dimension = {
-      value: value,
-      column: data.cols[pivotCol],
-    };
-    // delete colDef.id
-    return colDef;
   });
 
   return {
diff --git a/frontend/src/metabase/visualizations/components/ChartSettings.jsx b/frontend/src/metabase/visualizations/components/ChartSettings.jsx
index 22aa9b87704dc0e014ca938d7e8a2b40b9e5e47c..1e49a9766f8605d1e3de7fffcdbc5816f99a4966 100644
--- a/frontend/src/metabase/visualizations/components/ChartSettings.jsx
+++ b/frontend/src/metabase/visualizations/components/ChartSettings.jsx
@@ -5,6 +5,8 @@ import _ from "underscore";
 import { t } from "c-3po";
 import Warnings from "metabase/query_builder/components/Warnings.jsx";
 
+import Button from "metabase/components/Button";
+
 import Visualization from "metabase/visualizations/components/Visualization.jsx";
 import { getSettingsWidgets } from "metabase/visualizations/lib/settings";
 import MetabaseAnalytics from "metabase/lib/analytics";
@@ -13,31 +15,6 @@ import {
   extractRemappings,
 } from "metabase/visualizations";
 
-const ChartSettingsTab = ({ name, active, onClick }) => (
-  <a
-    className={cx("block text-brand py1 text-centered", {
-      "bg-brand text-white": active,
-    })}
-    onClick={() => onClick(name)}
-  >
-    {name.toUpperCase()}
-  </a>
-);
-
-const ChartSettingsTabs = ({ tabs, selectTab, activeTab }) => (
-  <ul className="bordered rounded flex justify-around overflow-hidden">
-    {tabs.map((tab, index) => (
-      <li className="flex-full border-left" key={index}>
-        <ChartSettingsTab
-          name={tab}
-          active={tab === activeTab}
-          onClick={selectTab}
-        />
-      </li>
-    ))}
-  </ul>
-);
-
 const Widget = ({
   title,
   hidden,
@@ -67,9 +44,19 @@ class ChartSettings extends Component {
     };
   }
 
-  selectTab = tab => {
-    this.setState({ currentTab: tab });
-  };
+  getChartTypeName() {
+    let { CardVisualization } = getVisualizationTransformed(this.props.series);
+    switch (CardVisualization.identifier) {
+      case "table":
+        return "table";
+      case "scalar":
+        return "number";
+      case "funnel":
+        return "funnel";
+      default:
+        return "chart";
+    }
+  }
 
   _getSeries(series, settings) {
     if (settings) {
@@ -79,7 +66,11 @@ class ChartSettings extends Component {
     return transformed.series;
   }
 
-  onResetSettings = () => {
+  handleSelectTab = tab => {
+    this.setState({ currentTab: tab });
+  };
+
+  handleResetSettings = () => {
     MetabaseAnalytics.trackEvent("Chart Settings", "Reset Settings");
     this.setState({
       settings: {},
@@ -87,7 +78,7 @@ class ChartSettings extends Component {
     });
   };
 
-  onChangeSettings = newSettings => {
+  handleChangeSettings = newSettings => {
     for (const key of Object.keys(newSettings)) {
       MetabaseAnalytics.trackEvent("Chart Settings", "Change Setting", key);
     }
@@ -101,33 +92,23 @@ class ChartSettings extends Component {
     });
   };
 
-  onDone() {
+  handleDone = () => {
     this.props.onChange(this.state.settings);
     this.props.onClose();
-  }
+  };
 
-  getChartTypeName() {
-    let { CardVisualization } = getVisualizationTransformed(this.props.series);
-    switch (CardVisualization.identifier) {
-      case "table":
-        return "table";
-      case "scalar":
-        return "number";
-      case "funnel":
-        return "funnel";
-      default:
-        return "chart";
-    }
-  }
+  handleCancel = () => {
+    this.props.onClose();
+  };
 
   render() {
-    const { onClose, isDashboard } = this.props;
+    const { isDashboard } = this.props;
     const { series } = this.state;
 
     const tabs = {};
     for (const widget of getSettingsWidgets(
       series,
-      this.onChangeSettings,
+      this.handleChangeSettings,
       isDashboard,
     )) {
       tabs[widget.section] = tabs[widget.section] || [];
@@ -146,68 +127,97 @@ class ChartSettings extends Component {
     const widgets = tabs[currentTab];
 
     return (
-      <div className="flex flex-column spread p4">
-        <h2 className="my2">{t`Customize this ${this.getChartTypeName()}`}</h2>
-
+      <div className="flex flex-column spread">
         {tabNames.length > 1 && (
-          <ChartSettingsTabs
-            tabs={tabNames}
-            selectTab={this.selectTab}
-            activeTab={currentTab}
-          />
-        )}
-        <div className="Grid flex-full mt3">
-          <div className="Grid-cell Cell--1of3 scroll-y p1">
-            {widgets &&
-              widgets.map(widget => <Widget key={widget.id} {...widget} />)}
+          <div className="border-bottom flex flex-no-shrink pl4">
+            {tabNames.map(tabName => (
+              <div
+                className={cx(
+                  "h3 py2 mr2 border-bottom cursor-pointer text-brand-hover border-brand-hover",
+                  {
+                    "text-brand border-brand": currentTab === tabName,
+                    "border-transparent": currentTab !== tabName,
+                  },
+                )}
+                style={{ borderWidth: 3 }}
+                onClick={() => this.handleSelectTab(tabName)}
+              >
+                {tabName}
+              </div>
+            ))}
           </div>
-          <div className="Grid-cell flex flex-column">
-            <div className="flex flex-column">
-              <Warnings
-                className="mx2 align-self-end text-gold"
-                warnings={this.state.warnings}
-                size={20}
-              />
+        )}
+        <div className="full-height relative">
+          <div className="Grid spread">
+            <div className="Grid-cell Cell--1of3 scroll-y scroll-show border-right p4">
+              {widgets &&
+                widgets.map(widget => (
+                  <Widget key={`${widget.id}`} {...widget} />
+                ))}
             </div>
-            <div className="flex-full relative">
-              <Visualization
-                className="spread"
-                rawSeries={series}
-                isEditing
-                showTitle
-                isDashboard
-                showWarnings
-                onUpdateVisualizationSettings={this.onChangeSettings}
-                onUpdateWarnings={warnings => this.setState({ warnings })}
+            <div className="Grid-cell flex flex-column pt2">
+              <div className="mx4 flex flex-column">
+                <Warnings
+                  className="mx2 align-self-end text-gold"
+                  warnings={this.state.warnings}
+                  size={20}
+                />
+              </div>
+              <div className="mx4 flex-full relative">
+                <Visualization
+                  className="spread"
+                  rawSeries={series}
+                  isEditing
+                  showTitle
+                  isDashboard
+                  showWarnings
+                  onUpdateVisualizationSettings={this.handleChangeSettings}
+                  onUpdateWarnings={warnings => this.setState({ warnings })}
+                />
+              </div>
+              <ChartSettingsFooter
+                onDone={this.handleDone}
+                onCancel={this.handleCancel}
+                onReset={
+                  !_.isEqual(this.state.settings, {})
+                    ? this.handleResetSettings
+                    : null
+                }
               />
             </div>
           </div>
         </div>
-        <div className="pt1">
-          {!_.isEqual(this.state.settings, {}) && (
-            <a
-              className="Button Button--danger float-right"
-              onClick={this.onResetSettings}
-              data-metabase-event="Chart Settings;Reset"
-            >{t`Reset to defaults`}</a>
-          )}
-
-          <div className="float-left">
-            <a
-              className="Button Button--primary ml2"
-              onClick={() => this.onDone()}
-              data-metabase-event="Chart Settings;Done"
-            >{t`Done`}</a>
-            <a
-              className="Button ml2"
-              onClick={onClose}
-              data-metabase-event="Chart Settings;Cancel"
-            >{t`Cancel`}</a>
-          </div>
-        </div>
       </div>
     );
   }
 }
 
+const ChartSettingsFooter = ({ className, onDone, onCancel, onReset }) => (
+  <div className={cx("py2 px4", className)}>
+    <div className="float-right">
+      <Button
+        className="ml2"
+        onClick={onCancel}
+        data-metabase-event="Chart Settings;Cancel"
+      >{t`Cancel`}</Button>
+      <Button
+        primary
+        className="ml2"
+        onClick={onDone}
+        data-metabase-event="Chart Settings;Done"
+      >{t`Done`}</Button>
+    </div>
+
+    {onReset && (
+      <Button
+        borderless
+        icon="refresh"
+        className="float-right ml2"
+        data-metabase-event="Chart Settings;Reset"
+        onClick={onReset}
+      >{t`Reset to defaults`}</Button>
+    )}
+  </div>
+);
+
 export default ChartSettings;
diff --git a/frontend/src/metabase/visualizations/components/TableInteractive.jsx b/frontend/src/metabase/visualizations/components/TableInteractive.jsx
index 5636d1f62e5271b4f9e586ccb7809f470fdac0ed..1e99af19e35f3d1be648336c400ea63eb78c2241 100644
--- a/frontend/src/metabase/visualizations/components/TableInteractive.jsx
+++ b/frontend/src/metabase/visualizations/components/TableInteractive.jsx
@@ -294,9 +294,10 @@ export default class TableInteractive extends Component {
   }
 
   cellRenderer = ({ key, style, rowIndex, columnIndex }: CellRendererProps) => {
-    const { data, isPivoted } = this.props;
+    const { data, isPivoted, settings } = this.props;
     const { dragColIndex } = this.state;
     const { rows, cols } = data;
+    const getCellBackgroundColor = settings["table._cell_background_getter"];
 
     const column = cols[columnIndex];
     const row = rows[rowIndex];
@@ -309,6 +310,9 @@ export default class TableInteractive extends Component {
       isPivoted,
     );
     const isClickable = this.visualizationIsClickable(clicked);
+    const backgroundColor =
+      getCellBackgroundColor &&
+      getCellBackgroundColor(value, rowIndex, column.name);
 
     return (
       <div
@@ -319,6 +323,7 @@ export default class TableInteractive extends Component {
           left: this.getColumnLeft(style, columnIndex),
           // add a transition while dragging column
           transition: dragColIndex != null ? "left 200ms" : null,
+          backgroundColor,
         }}
         className={cx("TableInteractive-cellWrapper", {
           "TableInteractive-cellWrapper--firstColumn": columnIndex === 0,
diff --git a/frontend/src/metabase/visualizations/components/TableSimple.jsx b/frontend/src/metabase/visualizations/components/TableSimple.jsx
index 2cc16e6fe42420b59b2c9414311c50621ca0306c..c36f0fec2d348b0ae6585d5d49e66856378edadb 100644
--- a/frontend/src/metabase/visualizations/components/TableSimple.jsx
+++ b/frontend/src/metabase/visualizations/components/TableSimple.jsx
@@ -90,8 +90,10 @@ export default class TableSimple extends Component {
       onVisualizationClick,
       visualizationIsClickable,
       isPivoted,
+      settings,
     } = this.props;
     const { rows, cols } = data;
+    const getCellBackgroundColor = settings["table._cell_background_getter"];
 
     const { page, pageSize, sortColumn, sortDescending } = this.state;
 
@@ -169,7 +171,16 @@ export default class TableSimple extends Component {
                       return (
                         <td
                           key={columnIndex}
-                          style={{ whiteSpace: "nowrap" }}
+                          style={{
+                            whiteSpace: "nowrap",
+                            backgroundColor:
+                              getCellBackgroundColor &&
+                              getCellBackgroundColor(
+                                cell,
+                                rowIndex,
+                                cols[columnIndex].name,
+                              ),
+                          }}
                           className={cx("px1 border-bottom", {
                             "text-right": isColumnRightAligned(
                               cols[columnIndex],
diff --git a/frontend/src/metabase/visualizations/components/Visualization.jsx b/frontend/src/metabase/visualizations/components/Visualization.jsx
index b5254e7f108257a3cb1b89ca17e2c63a3d8cb57c..863c27d0a3fce3e3d717986df072dc50d884469a 100644
--- a/frontend/src/metabase/visualizations/components/Visualization.jsx
+++ b/frontend/src/metabase/visualizations/components/Visualization.jsx
@@ -301,7 +301,9 @@ export default class Visualization extends Component {
   };
 
   hideActions = () => {
-    this.setState({ clicked: null });
+    if (this.state.clicked !== null) {
+      this.setState({ clicked: null });
+    }
   };
 
   render() {
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedFields.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedFields.jsx
index a55ed1b539da338136c84399a9f8df349f20008a..94e9290306fbaa1c2e2337eecc55f2aa66c86a46 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedFields.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedFields.jsx
@@ -2,118 +2,108 @@ import React, { Component } from "react";
 
 import CheckBox from "metabase/components/CheckBox.jsx";
 import Icon from "metabase/components/Icon.jsx";
-import { sortable } from "react-sortable";
+
+import { SortableContainer, SortableElement } from "react-sortable-hoc";
 
 import cx from "classnames";
+import _ from "underscore";
 
-@sortable
-class OrderedFieldListItem extends Component {
-  render() {
+const SortableField = SortableElement(
+  ({ field, columnNames, onSetEnabled }) => (
+    <div
+      className={cx("flex align-center p1", {
+        "text-grey-2": !field.enabled,
+      })}
+    >
+      <CheckBox
+        checked={field.enabled}
+        onChange={e => onSetEnabled(e.target.checked)}
+      />
+      <span className="ml1 h4">{columnNames[field.name]}</span>
+      <Icon
+        className="flex-align-right text-grey-2 mr1 cursor-pointer"
+        name="grabber"
+        width={14}
+        height={14}
+      />
+    </div>
+  ),
+);
+
+const SortableFieldList = SortableContainer(
+  ({ fields, columnNames, onSetEnabled }) => {
     return (
-      <div {...this.props} className="list-item">
-        {this.props.children}
+      <div>
+        {fields.map((field, index) => (
+          <SortableField
+            key={`item-${index}`}
+            index={index}
+            field={field}
+            columnNames={columnNames}
+            onSetEnabled={enabled => onSetEnabled(index, enabled)}
+          />
+        ))}
       </div>
     );
-  }
-}
+  },
+);
 
 export default class ChartSettingOrderedFields extends Component {
-  constructor(props) {
-    super(props);
-    this.state = {
-      draggingIndex: null,
-      data: { items: [...this.props.value] },
-    };
-  }
-
-  componentWillReceiveProps(nextProps) {
-    this.setState({ data: { items: [...nextProps.value] } });
-  }
-
-  updateState = obj => {
-    this.setState(obj);
-    if (obj.draggingIndex == null) {
-      this.props.onChange([...this.state.data.items]);
-    }
+  handleSetEnabled = (index, checked) => {
+    const fields = [...this.props.value];
+    fields[index] = { ...fields[index], enabled: checked };
+    this.props.onChange(fields);
   };
 
-  setEnabled = (index, checked) => {
-    const items = [...this.state.data.items];
-    items[index] = { ...items[index], enabled: checked };
-    this.setState({ data: { items } });
-    this.props.onChange([...items]);
+  handleToggleAll = anyEnabled => {
+    const fields = this.props.value.map(field => ({
+      ...field,
+      enabled: !anyEnabled,
+    }));
+    this.props.onChange([...fields]);
   };
 
-  isAnySelected = () => {
-    let selected = false;
-    for (const item of [...this.state.data.items]) {
-      if (item.enabled) {
-        selected = true;
-        break;
-      }
-    }
-    return selected;
+  handleSortEnd = ({ oldIndex, newIndex }) => {
+    const fields = [...this.props.value];
+    fields.splice(newIndex, 0, fields.splice(oldIndex, 1)[0]);
+    this.props.onChange(fields);
   };
 
-  toggleAll = anySelected => {
-    const items = [...this.state.data.items].map(item => ({
-      ...item,
-      enabled: !anySelected,
-    }));
-    this.setState({ data: { items } });
-    this.props.onChange([...items]);
+  isAnySelected = () => {
+    const { value } = this.props;
+    return _.any(value, field => field.enabled);
   };
 
   render() {
-    const { columnNames } = this.props;
-    const anySelected = this.isAnySelected();
+    const { value, columnNames } = this.props;
+    const anyEnabled = this.isAnySelected();
     return (
       <div className="list">
         <div className="toggle-all">
           <div
             className={cx("flex align-center p1", {
-              "text-grey-2": !anySelected,
+              "text-grey-2": !anyEnabled,
             })}
           >
             <CheckBox
-              checked={anySelected}
-              className={cx("text-brand", { "text-grey-2": !anySelected })}
-              onChange={e => this.toggleAll(anySelected)}
+              checked={anyEnabled}
+              className={cx("text-brand", { "text-grey-2": !anyEnabled })}
+              onChange={e => this.handleToggleAll(anyEnabled)}
               invertChecked
             />
             <span className="ml1 h4">
-              {anySelected ? "Unselect all" : "Select all"}
+              {anyEnabled ? "Unselect all" : "Select all"}
             </span>
           </div>
         </div>
-        {this.state.data.items.map((item, i) => (
-          <OrderedFieldListItem
-            key={i}
-            updateState={this.updateState}
-            items={this.state.data.items}
-            draggingIndex={this.state.draggingIndex}
-            sortId={i}
-            outline="list"
-          >
-            <div
-              className={cx("flex align-center p1", {
-                "text-grey-2": !item.enabled,
-              })}
-            >
-              <CheckBox
-                checked={item.enabled}
-                onChange={e => this.setEnabled(i, e.target.checked)}
-              />
-              <span className="ml1 h4">{columnNames[item.name]}</span>
-              <Icon
-                className="flex-align-right text-grey-2 mr1 cursor-pointer"
-                name="grabber"
-                width={14}
-                height={14}
-              />
-            </div>
-          </OrderedFieldListItem>
-        ))}
+        <SortableFieldList
+          fields={value}
+          columnNames={columnNames}
+          onSetEnabled={this.handleSetEnabled}
+          onSortEnd={this.handleSortEnd}
+          distance={5}
+          helperClass="z5"
+        />
       </div>
     );
   }
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingsTableFormatting.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingsTableFormatting.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..0d669b714558247e613ce6e7574d7419f4d2fbbc
--- /dev/null
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingsTableFormatting.jsx
@@ -0,0 +1,426 @@
+import React from "react";
+
+import { t, jt } from "c-3po";
+
+import Button from "metabase/components/Button";
+import Icon from "metabase/components/Icon";
+import Select, { Option } from "metabase/components/Select";
+import Radio from "metabase/components/Radio";
+import Toggle from "metabase/components/Toggle";
+import ColorPicker from "metabase/components/ColorPicker";
+import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
+
+import { SortableContainer, SortableElement } from "react-sortable-hoc";
+
+import { formatNumber, capitalize } from "metabase/lib/formatting";
+import { isNumeric } from "metabase/lib/schema_metadata";
+
+import _ from "underscore";
+import d3 from "d3";
+import cx from "classnames";
+
+const OPERATOR_NAMES = {
+  "<": t`less than`,
+  ">": t`greater than`,
+  "<=": t`less than or equal to`,
+  ">=": t`greater than or equal to`,
+  "=": t`equal to`,
+  "!=": t`not equal to`,
+};
+
+import { desaturated as colors, getColorScale } from "metabase/lib/colors";
+
+const COLORS = Object.values(colors);
+const COLOR_RANGES = [].concat(
+  ...COLORS.map(color => [["white", color], [color, "white"]]),
+  [
+    [colors.red, "white", colors.green],
+    [colors.green, "white", colors.red],
+    [colors.red, colors.yellow, colors.green],
+    [colors.green, colors.yellow, colors.red],
+  ],
+);
+
+const DEFAULTS_BY_TYPE = {
+  single: {
+    columns: [],
+    type: "single",
+    operator: ">",
+    value: 0,
+    color: COLORS[0],
+    highlight_row: false,
+  },
+  range: {
+    columns: [],
+    type: "range",
+    colors: COLOR_RANGES[0],
+    min_type: null,
+    max_type: null,
+    min_value: 0,
+    max_value: 100,
+  },
+};
+
+// predicate for columns that can be formatted
+export const isFormattable = isNumeric;
+
+export default class ChartSettingsTableFormatting extends React.Component {
+  state = {
+    editingRule: null,
+    editingRuleIsNew: null,
+  };
+  render() {
+    const { value, onChange, cols } = this.props;
+    const { editingRule, editingRuleIsNew } = this.state;
+    if (editingRule !== null && value[editingRule]) {
+      return (
+        <RuleEditor
+          rule={value[editingRule]}
+          cols={cols}
+          isNew={editingRuleIsNew}
+          onChange={rule =>
+            onChange([
+              ...value.slice(0, editingRule),
+              rule,
+              ...value.slice(editingRule + 1),
+            ])
+          }
+          onRemove={() => {
+            onChange([
+              ...value.slice(0, editingRule),
+              ...value.slice(editingRule + 1),
+            ]);
+            this.setState({ editingRule: null, editingRuleIsNew: null });
+          }}
+          onDone={() => {
+            this.setState({ editingRule: null, editingRuleIsNew: null });
+          }}
+        />
+      );
+    } else {
+      return (
+        <RuleListing
+          rules={value}
+          cols={cols}
+          onEdit={index => {
+            this.setState({ editingRule: index, editingRuleIsNew: false });
+          }}
+          onAdd={() => {
+            onChange([
+              {
+                ...DEFAULTS_BY_TYPE["single"],
+                // if there's a single column use that by default
+                columns: cols.length === 1 ? [cols[0].name] : [],
+              },
+              ...value,
+            ]);
+            this.setState({ editingRule: 0, editingRuleIsNew: true });
+          }}
+          onRemove={index =>
+            onChange([...value.slice(0, index), ...value.slice(index + 1)])
+          }
+          onMove={(from, to) => {
+            const newValue = [...value];
+            newValue.splice(to, 0, newValue.splice(from, 1)[0]);
+            onChange(newValue);
+          }}
+        />
+      );
+    }
+  }
+}
+
+const SortableRuleItem = SortableElement(({ rule, cols, onEdit, onRemove }) => (
+  <RulePreview rule={rule} cols={cols} onClick={onEdit} onRemove={onRemove} />
+));
+
+const SortableRuleList = SortableContainer(
+  ({ rules, cols, onEdit, onRemove }) => {
+    return (
+      <div>
+        {rules.map((rule, index) => (
+          <SortableRuleItem
+            key={`item-${index}`}
+            index={index}
+            rule={rule}
+            cols={cols}
+            onEdit={() => onEdit(index)}
+            onRemove={() => onRemove(index)}
+          />
+        ))}
+      </div>
+    );
+  },
+);
+
+const RuleListing = ({ rules, cols, onEdit, onAdd, onRemove, onMove }) => (
+  <div>
+    <h3>{t`Conditional formatting`}</h3>
+    <div className="mt2">
+      {t`You can add rules to make the cells in this table change color if
+    they meet certain conditions.`}
+    </div>
+    <div className="mt2">
+      <Button borderless icon="add" onClick={onAdd}>
+        {t`Add a rule`}
+      </Button>
+    </div>
+    {rules.length > 0 ? (
+      <div className="mt2">
+        <h3>{t`Rules will be applied in this order`}</h3>
+        <div className="mt2">{t`Click and drag to reorder.`}</div>
+        <SortableRuleList
+          rules={rules}
+          cols={cols}
+          onEdit={onEdit}
+          onRemove={onRemove}
+          onSortEnd={({ oldIndex, newIndex }) => onMove(oldIndex, newIndex)}
+          distance={10}
+          helperClass="z5"
+        />
+      </div>
+    ) : null}
+  </div>
+);
+
+const RulePreview = ({ rule, cols, onClick, onRemove }) => (
+  <div
+    className="my2 bordered rounded shadowed cursor-pointer overflow-hidden bg-white"
+    onClick={onClick}
+  >
+    <div className="p1 border-bottom relative bg-grey-0">
+      <div className="px1 flex align-center relative">
+        <span className="h4 flex-full text-dark">
+          {rule.columns.length > 0 ? (
+            rule.columns
+              .map(
+                name =>
+                  (_.findWhere(cols, { name }) || {}).display_name || name,
+              )
+              .join(", ")
+          ) : (
+            <span
+              style={{ fontStyle: "oblique" }}
+            >{t`No columns selected`}</span>
+          )}
+        </span>
+        <Icon
+          name="close"
+          className="cursor-pointer text-grey-2 text-grey-4-hover"
+          onClick={e => {
+            e.stopPropagation();
+            onRemove();
+          }}
+        />
+      </div>
+    </div>
+    <div className="p2 flex align-center">
+      <RuleBackground
+        rule={rule}
+        className={cx(
+          "mr2 flex-no-shrink rounded overflow-hidden border-grey-1",
+          { bordered: rule.type === "range" },
+        )}
+        style={{ width: 40, height: 40 }}
+      />
+      <RuleDescription rule={rule} />
+    </div>
+  </div>
+);
+
+const RuleBackground = ({ rule, className, style }) =>
+  rule.type === "range" ? (
+    <RangePreview colors={rule.colors} className={className} style={style} />
+  ) : rule.type === "single" ? (
+    <SinglePreview color={rule.color} className={className} style={style} />
+  ) : null;
+
+const SinglePreview = ({ color, className, style, ...props }) => (
+  <div
+    className={className}
+    style={{ ...style, background: color }}
+    {...props}
+  />
+);
+
+const RangePreview = ({ colors = [], sections = 5, className, ...props }) => {
+  const scale = getColorScale([0, sections - 1], colors);
+  return (
+    <div className={cx(className, "flex")} {...props}>
+      {d3
+        .range(0, sections)
+        .map(value => (
+          <div className="flex-full" style={{ background: scale(value) }} />
+        ))}
+    </div>
+  );
+};
+
+const RuleDescription = ({ rule }) => (
+  <span>
+    {rule.type === "range"
+      ? t`Cells in this column will be tinted based on their values.`
+      : rule.type === "single"
+        ? jt`When a cell in these columns is ${(
+            <span className="text-bold">
+              {OPERATOR_NAMES[rule.operator]} {formatNumber(rule.value)}
+            </span>
+          )} it will be tinted this color.`
+        : null}
+  </span>
+);
+
+const RuleEditor = ({ rule, cols, isNew, onChange, onDone, onRemove }) => (
+  <div>
+    <h3 className="mb1">{t`Which columns should be affected?`}</h3>
+    <Select
+      value={rule.columns}
+      onChange={e => onChange({ ...rule, columns: e.target.value })}
+      isInitiallyOpen={rule.columns.length === 0}
+      placeholder="Choose a column"
+      multiple
+    >
+      {cols.map(col => <Option value={col.name}>{col.display_name}</Option>)}
+    </Select>
+    <h3 className="mt3 mb1">{t`Formatting style`}</h3>
+    <Radio
+      value={rule.type}
+      options={[
+        { name: t`Single color`, value: "single" },
+        { name: t`Color range`, value: "range" },
+      ]}
+      onChange={type => onChange({ ...DEFAULTS_BY_TYPE[type], ...rule, type })}
+      isVertical
+    />
+    {rule.type === "single" ? (
+      <div>
+        <h3 className="mt3 mb1">{t`When a cell in this column is…`}</h3>
+        <Select
+          value={rule.operator}
+          onChange={e => onChange({ ...rule, operator: e.target.value })}
+        >
+          {Object.entries(OPERATOR_NAMES).map(([operator, operatorName]) => (
+            <Option value={operator}>{capitalize(operatorName)}</Option>
+          ))}
+        </Select>
+        <NumericInput
+          value={rule.value}
+          onChange={value => onChange({ ...rule, value })}
+        />
+        <h3 className="mt3 mb1">{t`…turn its background this color:`}</h3>
+        <ColorPicker
+          value={rule.color}
+          colors={COLORS}
+          onChange={color => onChange({ ...rule, color })}
+        />
+        <h3 className="mt3 mb1">{t`Highlight the whole row`}</h3>
+        <Toggle
+          value={rule.highlight_row}
+          onChange={highlight_row => onChange({ ...rule, highlight_row })}
+        />
+      </div>
+    ) : rule.type === "range" ? (
+      <div>
+        <h3 className="mt3 mb1">{t`Colors`}</h3>
+        <ColorRangePicker
+          colors={rule.colors}
+          onChange={colors => onChange({ ...rule, colors })}
+        />
+        <h3 className="mt3 mb1">{t`Start the range at`}</h3>
+        <Radio
+          value={rule.min_type}
+          onChange={min_type => onChange({ ...rule, min_type })}
+          options={(rule.columns.length <= 1
+            ? [{ name: t`Smallest value in this column`, value: null }]
+            : [
+                { name: t`Smallest value in each column`, value: null },
+                {
+                  name: t`Smallest value in all of these columns`,
+                  value: "all",
+                },
+              ]
+          ).concat([{ name: t`Custom value`, value: "custom" }])}
+          isVertical
+        />
+        {rule.min_type === "custom" && (
+          <NumericInput
+            value={rule.min_value}
+            onChange={min_value => onChange({ ...rule, min_value })}
+          />
+        )}
+        <h3 className="mt3 mb1">{t`End the range at`}</h3>
+        <Radio
+          value={rule.max_type}
+          onChange={max_type => onChange({ ...rule, max_type })}
+          options={(rule.columns.length <= 1
+            ? [{ name: t`Largest value in this column`, value: null }]
+            : [
+                { name: t`Largest value in each column`, value: null },
+                {
+                  name: t`Largest value in all of these columns`,
+                  value: "all",
+                },
+              ]
+          ).concat([{ name: t`Custom value`, value: "custom" }])}
+          isVertical
+        />
+        {rule.max_type === "custom" && (
+          <NumericInput
+            value={rule.max_value}
+            onChange={max_value => onChange({ ...rule, max_value })}
+          />
+        )}
+      </div>
+    ) : null}
+    <div className="mt4">
+      {rule.columns.length === 0 ? (
+        <Button primary onClick={onRemove}>
+          {isNew ? t`Cancel` : t`Delete`}
+        </Button>
+      ) : (
+        <Button primary onClick={onDone}>
+          {isNew ? t`Add rule` : t`Update rule`}
+        </Button>
+      )}
+    </div>
+  </div>
+);
+
+const ColorRangePicker = ({ colors, onChange, className, style }) => (
+  <PopoverWithTrigger
+    triggerElement={
+      <RangePreview
+        colors={colors}
+        className={cx(className, "bordered rounded overflow-hidden")}
+        style={{ height: 30, ...style }}
+      />
+    }
+  >
+    {({ onClose }) => (
+      <div className="pt1 mr1 flex flex-wrap" style={{ width: 300 }}>
+        {COLOR_RANGES.map(range => (
+          <div className={"mb1 pl1"} style={{ flex: "1 1 50%" }}>
+            <RangePreview
+              colors={range}
+              onClick={() => {
+                onChange(range);
+                onClose();
+              }}
+              className={cx("bordered rounded overflow-hidden cursor-pointer")}
+              style={{ height: 30 }}
+            />
+          </div>
+        ))}
+      </div>
+    )}
+  </PopoverWithTrigger>
+);
+
+const NumericInput = ({ value, onChange }) => (
+  <input
+    className="AdminSelect input mt1 full"
+    type="number"
+    value={value}
+    onChange={e => onChange(parseFloat(e.target.value))}
+  />
+);
diff --git a/frontend/src/metabase/visualizations/visualizations/Table.jsx b/frontend/src/metabase/visualizations/visualizations/Table.jsx
index dd5289924cbdb321526fe560f9872c035e079e5d..0ec7fef9f0ab3e0b24d5dc1bfa9cde5b49d493e3 100644
--- a/frontend/src/metabase/visualizations/visualizations/Table.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Table.jsx
@@ -14,15 +14,26 @@ import {
   getFriendlyName,
 } from "metabase/visualizations/lib/utils";
 import ChartSettingOrderedFields from "metabase/visualizations/components/settings/ChartSettingOrderedFields.jsx";
+import ChartSettingsTableFormatting, {
+  isFormattable,
+} from "metabase/visualizations/components/settings/ChartSettingsTableFormatting.jsx";
 
 import _ from "underscore";
 import cx from "classnames";
+import d3 from "d3";
+import Color from "color";
+import { getColorScale } from "metabase/lib/colors";
+
 import RetinaImage from "react-retina-image";
 import { getIn } from "icepick";
 
 import type { DatasetData } from "metabase/meta/types/Dataset";
 import type { Card, VisualizationSettings } from "metabase/meta/types/Card";
 
+const CELL_ALPHA = 0.65;
+const ROW_ALPHA = 0.2;
+const GRADIENT_ALPHA = 0.75;
+
 type Props = {
   card: Card,
   data: DatasetData,
@@ -33,6 +44,115 @@ type State = {
   data: ?DatasetData,
 };
 
+const alpha = (color, amount) =>
+  Color(color)
+    .alpha(amount)
+    .string();
+
+function compileFormatter(
+  format,
+  columnName,
+  columnExtents,
+  isRowFormatter = false,
+) {
+  if (format.type === "single") {
+    let { operator, value, color } = format;
+    if (isRowFormatter) {
+      color = alpha(color, ROW_ALPHA);
+    } else {
+      color = alpha(color, CELL_ALPHA);
+    }
+    switch (operator) {
+      case "<":
+        return v => (v < value ? color : null);
+      case "<=":
+        return v => (v <= value ? color : null);
+      case ">=":
+        return v => (v >= value ? color : null);
+      case ">":
+        return v => (v > value ? color : null);
+      case "=":
+        return v => (v === value ? color : null);
+      case "!=":
+        return v => (v !== value ? color : null);
+    }
+  } else if (format.type === "range") {
+    const columnMin = name =>
+      columnExtents && columnExtents[name] && columnExtents[name][0];
+    const columnMax = name =>
+      columnExtents && columnExtents[name] && columnExtents[name][1];
+
+    const min =
+      format.min_type === "custom"
+        ? format.min_value
+        : format.min_type === "all"
+          ? Math.min(...format.columns.map(columnMin))
+          : columnMin(columnName);
+    const max =
+      format.max_type === "custom"
+        ? format.max_value
+        : format.max_type === "all"
+          ? Math.max(...format.columns.map(columnMax))
+          : columnMax(columnName);
+
+    if (typeof max !== "number" || typeof min !== "number") {
+      console.warn("Invalid range min/max", min, max);
+      return () => null;
+    }
+
+    return getColorScale(
+      [min, max],
+      format.colors.map(c => alpha(c, GRADIENT_ALPHA)),
+    ).clamp(true);
+  } else {
+    console.warn("Unknown format type", format.type);
+    return () => null;
+  }
+}
+
+function computeColumnExtents(formats, data) {
+  return _.chain(formats)
+    .map(format => format.columns)
+    .flatten()
+    .uniq()
+    .map(columnName => {
+      const colIndex = _.findIndex(data.cols, col => col.name === columnName);
+      return [columnName, d3.extent(data.rows, row => row[colIndex])];
+    })
+    .object()
+    .value();
+}
+
+function compileFormatters(formats, columnExtents) {
+  const formatters = {};
+  for (const format of formats) {
+    for (const columnName of format.columns) {
+      formatters[columnName] = formatters[columnName] || [];
+      formatters[columnName].push(
+        compileFormatter(format, columnName, columnExtents, false),
+      );
+    }
+  }
+  return formatters;
+}
+
+function compileRowFormatters(formats) {
+  const rowFormatters = [];
+  for (const format of formats.filter(
+    format => format.type === "single" && format.highlight_row,
+  )) {
+    const formatter = compileFormatter(format, null, null, true);
+    if (formatter) {
+      for (const colName of format.columns) {
+        rowFormatters.push((row, colIndexes) =>
+          formatter(row[colIndexes[colName]]),
+        );
+      }
+    }
+  }
+  return rowFormatters;
+}
+
 export default class Table extends Component {
   props: Props;
   state: State;
@@ -53,6 +173,7 @@ export default class Table extends Component {
 
   static settings = {
     "table.pivot": {
+      section: "Data",
       title: t`Pivot the table`,
       widget: "toggle",
       getHidden: ([{ card, data }]) => data && data.cols.length !== 3,
@@ -64,6 +185,7 @@ export default class Table extends Component {
         data.cols.filter(isDimension).length === 2,
     },
     "table.columns": {
+      section: "Data",
       title: t`Fields to include`,
       widget: ChartSettingOrderedFields,
       getHidden: (series, vizSettings) => vizSettings["table.pivot"],
@@ -86,6 +208,65 @@ export default class Table extends Component {
       }),
     },
     "table.column_widths": {},
+    "table.column_formatting": {
+      section: "Formatting",
+      widget: ChartSettingsTableFormatting,
+      default: [],
+      getProps: ([{ data: { cols } }], settings) => ({
+        cols: cols.filter(isFormattable),
+        isPivoted: settings["table.pivot"],
+      }),
+      getHidden: ([{ data: { cols } }], settings) =>
+        cols.filter(isFormattable).length === 0,
+      readDependencies: ["table.pivot"],
+    },
+    "table._cell_background_getter": {
+      getValue([{ data }], settings) {
+        const { rows, cols } = data;
+        const formats = settings["table.column_formatting"];
+        const pivot = settings["table.pivot"];
+        let formatters = {};
+        let rowFormatters = [];
+        try {
+          const columnExtents = computeColumnExtents(formats, data);
+          formatters = compileFormatters(formats, columnExtents);
+          rowFormatters = compileRowFormatters(formats, columnExtents);
+        } catch (e) {
+          console.error(e);
+        }
+        const colIndexes = _.object(
+          cols.map((col, index) => [col.name, index]),
+        );
+        if (
+          Object.values(formatters).length === 0 &&
+          Object.values(formatters).length === 0
+        ) {
+          return null;
+        } else {
+          return function(value, rowIndex, colName) {
+            if (formatters[colName]) {
+              // const value = rows[rowIndex][colIndexes[colName]];
+              for (const formatter of formatters[colName]) {
+                const color = formatter(value);
+                if (color != null) {
+                  return color;
+                }
+              }
+            }
+            // don't highlight row for pivoted tables
+            if (!pivot) {
+              for (const rowFormatter of rowFormatters) {
+                const color = rowFormatter(rows[rowIndex], colIndexes);
+                if (color != null) {
+                  return color;
+                }
+              }
+            }
+          };
+        }
+      },
+      readDependencies: ["table.column_formatting", "table.pivot"],
+    },
   };
 
   constructor(props: Props) {
diff --git a/frontend/test/internal/__snapshots__/components.unit.spec.js.snap b/frontend/test/internal/__snapshots__/components.unit.spec.js.snap
index 17d5f57bcb0091b17103840ea6b13d5cfc03facd..8051d2faf039ec2971c4224d079f9fb4cb3c4f67 100644
--- a/frontend/test/internal/__snapshots__/components.unit.spec.js.snap
+++ b/frontend/test/internal/__snapshots__/components.unit.spec.js.snap
@@ -550,13 +550,11 @@ exports[`Select should render "Default" correctly 1`] = `
   style={undefined}
 >
   <div
-    className="AdminSelect border-med flex align-center  text-grey-3"
+    className="AdminSelect border-med flex align-center "
   >
     <span
       className="AdminSelect-content mr1"
-    >
-      Yellow
-    </span>
+    />
     <svg
       className="Icon Icon-chevrondown AdminSelect-chevron flex-align-right Icon-cxuQhR kTAgZA"
       fill="currentcolor"
@@ -585,13 +583,11 @@ exports[`Select should render "With search" correctly 1`] = `
   style={undefined}
 >
   <div
-    className="AdminSelect border-med flex align-center  text-grey-3"
+    className="AdminSelect border-med flex align-center "
   >
     <span
       className="AdminSelect-content mr1"
-    >
-      Yellow
-    </span>
+    />
     <svg
       className="Icon Icon-chevrondown AdminSelect-chevron flex-align-right Icon-cxuQhR kTAgZA"
       fill="currentcolor"
diff --git a/frontend/test/public/public.integ.spec.js b/frontend/test/public/public.integ.spec.js
index c79e27455fa0c5a5d509ae1c52b3ff805e524a27..ce047682ef17ae90f145dea1d6d56d8b3dc03821 100644
--- a/frontend/test/public/public.integ.spec.js
+++ b/frontend/test/public/public.integ.spec.js
@@ -51,6 +51,8 @@ import {
   FETCH_DASHBOARD_CARD_DATA,
   FETCH_CARD_DATA,
 } from "metabase/dashboard/dashboard";
+
+import Select from "metabase/components/Select";
 import RunButton from "metabase/query_builder/components/RunButton";
 import Scalar from "metabase/visualizations/visualizations/Scalar";
 import ParameterFieldWidget from "metabase/parameters/components/widgets/ParameterFieldWidget";
@@ -252,8 +254,10 @@ describe("public/embedded", () => {
           .last(),
       );
 
+      // currently only one Select is present, but verify it's the right one
+      expect(app.find(Select).text()).toBe("Disabled");
       // make the parameter editable
-      click(app.find(".AdminSelect-content[children='Disabled']"));
+      click(app.find(Select));
 
       click(app.find(".TestPopoverBody .Icon-pencil"));
 
diff --git a/frontend/test/visualizations/components/ChartSettings.unit.spec.js b/frontend/test/visualizations/components/ChartSettings.unit.spec.js
index 664f01602a36b2b69e6443a9ea610e8cc88c64e9..1729473d62e72e8cead0782338c111f02748057e 100644
--- a/frontend/test/visualizations/components/ChartSettings.unit.spec.js
+++ b/frontend/test/visualizations/components/ChartSettings.unit.spec.js
@@ -45,16 +45,12 @@ describe("ChartSettings", () => {
       it("should show null state", () => {
         const chartSettings = renderChartSettings();
 
-        expect(
-          chartSettings.find(".list-item [data-id=0] .Icon-check").length,
-        ).toEqual(1);
+        expect(chartSettings.find(".toggle-all .Icon-check").length).toEqual(1);
         expect(chartSettings.find("table").length).toEqual(1);
 
         click(chartSettings.find(".toggle-all .cursor-pointer"));
 
-        expect(
-          chartSettings.find(".list-item [data-id=0] .Icon-check").length,
-        ).toEqual(0);
+        expect(chartSettings.find(".toggle-all .Icon-check").length).toEqual(0);
         expect(chartSettings.find("table").length).toEqual(0);
         expect(chartSettings.text()).toContain(
           "Every field is hidden right now",
@@ -66,9 +62,7 @@ describe("ChartSettings", () => {
       it("should show all columns", () => {
         const chartSettings = renderChartSettings(false);
 
-        expect(
-          chartSettings.find(".list-item [data-id=0] .Icon-check").length,
-        ).toEqual(0);
+        expect(chartSettings.find(".toggle-all .Icon-check").length).toEqual(0);
         expect(chartSettings.find("table").length).toEqual(0);
         expect(chartSettings.text()).toContain(
           "Every field is hidden right now",
@@ -76,9 +70,7 @@ describe("ChartSettings", () => {
 
         click(chartSettings.find(".toggle-all .cursor-pointer"));
 
-        expect(
-          chartSettings.find(".list-item [data-id=0] .Icon-check").length,
-        ).toEqual(1);
+        expect(chartSettings.find(".toggle-all .Icon-check").length).toEqual(1);
         expect(chartSettings.find("table").length).toEqual(1);
         expect(chartSettings.text()).not.toContain(
           "Every field is hidden right now",
diff --git a/frontend/test/visualizations/components/settings/ChartSettingOrderedFields.unit.spec.js b/frontend/test/visualizations/components/settings/ChartSettingOrderedFields.unit.spec.js
index 5436c97fdf7d06cc6c036ff735e964ac6b6ede85..84cc84d4e8208c1af635c79955d2cf2d97b63c58 100644
--- a/frontend/test/visualizations/components/settings/ChartSettingOrderedFields.unit.spec.js
+++ b/frontend/test/visualizations/components/settings/ChartSettingOrderedFields.unit.spec.js
@@ -5,7 +5,7 @@ import ChartSettingOrderedFields from "metabase/visualizations/components/settin
 import { mount } from "enzyme";
 
 function renderChartSettingOrderedFields(props) {
-  return mount(<ChartSettingOrderedFields {...props} onChange={() => {}} />);
+  return mount(<ChartSettingOrderedFields onChange={() => {}} {...props} />);
 }
 
 describe("ChartSettingOrderedFields", () => {
@@ -40,16 +40,17 @@ describe("ChartSettingOrderedFields", () => {
   describe("toggleAll", () => {
     describe("when passed false", () => {
       it("should mark all fields as enabled", () => {
+        const onChange = jest.fn();
         const chartSettings = renderChartSettingOrderedFields({
           columnNames: { id: "ID", text: "Text" },
           value: [
             { name: "id", enabled: false },
             { name: "text", enabled: false },
           ],
+          onChange,
         });
-        const chartSettingsInstance = chartSettings.instance();
-        chartSettingsInstance.toggleAll(false);
-        expect(chartSettingsInstance.state.data.items).toEqual([
+        chartSettings.instance().handleToggleAll(false);
+        expect(onChange.mock.calls[0][0]).toEqual([
           { name: "id", enabled: true },
           { name: "text", enabled: true },
         ]);
@@ -58,17 +59,17 @@ describe("ChartSettingOrderedFields", () => {
 
     describe("when passed true", () => {
       it("should mark all fields as disabled", () => {
+        const onChange = jest.fn();
         const chartSettings = renderChartSettingOrderedFields({
           columnNames: { id: "ID", text: "Text" },
           value: [
             { name: "id", enabled: true },
             { name: "text", enabled: true },
           ],
+          onChange,
         });
-
-        const chartSettingsInstance = chartSettings.instance();
-        chartSettingsInstance.toggleAll(true);
-        expect(chartSettingsInstance.state.data.items).toEqual([
+        chartSettings.instance().handleToggleAll(true);
+        expect(onChange.mock.calls[0][0]).toEqual([
           { name: "id", enabled: false },
           { name: "text", enabled: false },
         ]);
diff --git a/package.json b/package.json
index 1f2148fee7e9fb30ced18b91951321057b0ce7b6..7ed080baa9e0892e12331632e31132bc32167556 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,7 @@
     "react-retina-image": "^2.0.5",
     "react-router": "3",
     "react-router-redux": "^4.0.8",
-    "react-sortable": "1.2",
+    "react-sortable-hoc": "^0.6.8",
     "react-textarea-autosize": "^5.2.1",
     "react-transition-group": "1",
     "react-virtualized": "^9.7.2",
diff --git a/yarn.lock b/yarn.lock
index 72c0df7836827015bf5f35e07d928eecc62c7e23..4796a9528c1de6dea3317c6f077321b44bf19523 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1277,7 +1277,7 @@ babel-register@^6.11.6, babel-register@^6.26.0:
     mkdirp "^0.5.1"
     source-map-support "^0.4.15"
 
-babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.9.0, babel-runtime@^6.9.1:
+babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.9.0, babel-runtime@^6.9.1:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
   dependencies:
@@ -6502,7 +6502,7 @@ lodash@^4.0.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, l
   version "4.17.4"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
 
-lodash@^4.17.5:
+lodash@^4.12.0, lodash@^4.17.5:
   version "4.17.10"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
 
@@ -9079,9 +9079,14 @@ react-router@3:
     prop-types "^15.5.6"
     warning "^3.0.0"
 
-react-sortable@1.2:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/react-sortable/-/react-sortable-1.2.0.tgz#5acd7e1910df665408957035acb5f2354519d849"
+react-sortable-hoc@^0.6.8:
+  version "0.6.8"
+  resolved "https://registry.yarnpkg.com/react-sortable-hoc/-/react-sortable-hoc-0.6.8.tgz#b08562f570d7c41f6e393fca52879d2ebb9118e9"
+  dependencies:
+    babel-runtime "^6.11.6"
+    invariant "^2.2.1"
+    lodash "^4.12.0"
+    prop-types "^15.5.7"
 
 react-test-renderer@15:
   version "15.6.2"