Repository: marimo-team/marimo

Issue:
Tiny altair plots in slides
### Describe the bug

Altair plots look great in the notebook view:

<img width="829" alt="Image" src="https://github.com/user-attachments/assets/23ec866b-d75a-44e1-9647-d35ce517cec6" />

However, when I open them on a slide, they're tiny (screenshot taken fullscreen on a MacOS 14"):

<img width="1502" alt="Image" src="https://github.com/user-attachments/assets/b687554f-c73d-4203-9721-1ed48bb946cd" />

I would expect them to fill the entire screen (unless there isn't any other content on the slide).



### Will you submit a PR?

- [ ] Yes

### Environment

<details>

```
{
  "marimo": "0.13.9",
  "OS": "Darwin",
  "OS Version": "24.4.0",
  "Processor": "arm",
  "Python Version": "3.12.8",
  "Binaries": {
    "Browser": "136.0.7103.93",
    "Node": "v20.18.1"
  },
  "Dependencies": {
    "click": "8.1.8",
    "docutils": "0.21.2",
    "itsdangerous": "2.2.0",
    "jedi": "0.19.2",
    "markdown": "3.8",
    "narwhals": "1.35.0",
    "packaging": "24.2",
    "psutil": "6.1.1",
    "pygments": "2.19.1",
    "pymdown-extensions": "10.14.3",
    "pyyaml": "6.0.2",
    "starlette": "0.46.2",
    "tomlkit": "0.13.2",
    "typing-extensions": "4.12.2",
    "uvicorn": "0.34.1",
    "websockets": "15.0.1"
  },
  "Optional Dependencies": {
    "altair": "5.5.0",
    "duckdb": "1.2.2",
    "nbformat": "5.10.4",
    "openai": "1.78.0",
    "pandas": "2.2.3",
    "polars": "1.27.1",
    "pyarrow": "18.1.0",
    "loro": "1.5.1",
    "pytest": "8.3.5",
    "python-lsp-ruff": "2.2.2",
    "python-lsp-server": "1.12.2",
    "ruff": "0.11.5",
    "sqlglot": "26.16.0"
  },
  "Experimental Flags": {}
}
```

</details>


### Code to reproduce

```python
import altair as alt
import marimo as mo
import json
import pandas as pd

cars = pd.read_json("https://vega.github.io/vega-datasets/data/cars.json")
chart = mo.ui.altair_chart(alt.Chart(cars).mark_point().encode(
    x='Horsepower',
    y='Miles_per_Gallon',
    color='Origin'
))
chart
```

Discussion:
@mscolnick: Can you add an explicit width to the chart? We center all out content on the slides and don't make everything full-screen (otherwise it can look off). 

Solution patch:
diff --git a/frontend/src/components/data-table/charts/components/chart-items.tsx b/frontend/src/components/data-table/charts/components/chart-items.tsx
index 250f5371..7ccf40c0 100644
--- a/frontend/src/components/data-table/charts/components/chart-items.tsx
+++ b/frontend/src/components/data-table/charts/components/chart-items.tsx
@@ -9,7 +9,7 @@ import {
   SelectItem,
   SelectValue,
 } from "@/components/ui/select";
-import { CHART_TYPE_ICON, COUNT_FIELD, EMPTY_VALUE } from "../constants";
+import { CHART_TYPE_ICON, COUNT_FIELD, type EMPTY_VALUE } from "../constants";
 import { ErrorBanner } from "@/plugins/impl/common/error-banner";
 import { buttonVariants } from "@/components/ui/button";
 import { useFormContext, useWatch } from "react-hook-form";
@@ -25,12 +25,88 @@ import {
   SortField,
   NumberField,
   BinFields,
+  type FieldName,
 } from "./form-fields";
-import { CHART_TYPES, type ChartType } from "../types";
+import { CHART_TYPES, type ChartType, type SelectableDataType } from "../types";
 import React from "react";
 import { FieldSection, Title } from "./layouts";
 import { useChartFormContext } from "../context";
 import { convertDataTypeToSelectable } from "../chart-spec/types";
+import type { DataType } from "@/core/kernel/messages";
+
+type SelectedDataType = SelectableDataType | typeof EMPTY_VALUE;
+type FieldDataType = DataType | typeof EMPTY_VALUE;
+
+// Utility functions for field type checking
+function isNonCountField(field?: {
+  field?: string;
+}) {
+  return isFieldSet(field?.field) && field?.field !== COUNT_FIELD;
+}
+
+function isNumberField(field?: {
+  field?: string;
+  selectedDataType?: SelectedDataType;
+}) {
+  return field?.selectedDataType === "number" && isNonCountField(field);
+}
+
+function isTemporalField(field?: {
+  field?: string;
+  selectedDataType?: SelectedDataType;
+}) {
+  return field?.selectedDataType === "temporal" && isNonCountField(field);
+}
+
+function isNonTemporalField(field?: {
+  field?: string;
+  selectedDataType?: SelectedDataType;
+}) {
+  return field?.selectedDataType !== "temporal" && isNonCountField(field);
+}
+
+// Helper to determine inferred and selected data types
+function getColumnDataTypes(column?: {
+  type?: FieldDataType;
+  selectedDataType?: SelectedDataType;
+}) {
+  const inferredDataType = column?.type
+    ? convertDataTypeToSelectable(column.type)
+    : "string";
+
+  return {
+    inferredDataType,
+    selectedDataType: column?.selectedDataType || inferredDataType,
+  };
+}
+
+const ColumnSelectorWithAggregation: React.FC<{
+  columnFieldName: FieldName;
+  column?: {
+    field?: string;
+    type?: FieldDataType;
+    selectedDataType?: SelectedDataType;
+  };
+  columns: Array<{ name: string; type: DataType }>;
+  binFieldName: FieldName;
+}> = ({ columnFieldName, column, columns, binFieldName }) => {
+  const { selectedDataType } = getColumnDataTypes(column);
+
+  return (
+    <div className="flex flex-row justify-between">
+      <ColumnSelector fieldName={columnFieldName} columns={columns} />
+      {isNonTemporalField(column) && (
+        <AggregationSelect
+          fieldName={
+            columnFieldName.replace(".field", ".aggregate") as FieldName
+          }
+          selectedDataType={selectedDataType}
+          binFieldName={binFieldName}
+        />
+      )}
+    </div>
+  );
+};
 
 export const ChartLoadingState: React.FC = () => (
   <div className="flex items-center gap-2 justify-center h-full w-full">
@@ -91,58 +167,38 @@ export const XAxis: React.FC = () => {
   const context = useChartFormContext();
 
   const xColumn = formValues.general?.xColumn;
-  const xColumnExists = isFieldSet(xColumn?.field);
-
-  const inferredXDataType = xColumn?.type
-    ? convertDataTypeToSelectable(xColumn.type)
-    : "string";
-
-  const selectedXDataType = xColumn?.selectedDataType || inferredXDataType;
-  const isXCountField = xColumn?.field === COUNT_FIELD;
-
-  const shouldShowXAggregation =
-    xColumnExists && selectedXDataType !== "temporal" && !isXCountField;
-
-  const shouldShowXTimeUnit =
-    xColumnExists && selectedXDataType === "temporal" && !isXCountField;
+  const { inferredDataType } = getColumnDataTypes(xColumn);
 
   return (
     <FieldSection>
       <Title text="X-Axis" />
-      <div className="flex flex-row justify-between">
-        <ColumnSelector
-          fieldName="general.xColumn.field"
-          columns={context.fields}
-        />
-        {shouldShowXAggregation && (
-          <AggregationSelect
-            fieldName="general.xColumn.aggregate"
-            selectedDataType={selectedXDataType}
-            binFieldName="xAxis.bin.binned"
-          />
-        )}
-      </div>
-      {xColumnExists && !isXCountField && (
+      <ColumnSelectorWithAggregation
+        columnFieldName="general.xColumn.field"
+        column={xColumn}
+        columns={context.fields}
+        binFieldName="xAxis.bin.binned"
+      />
+      {isNonCountField(xColumn) && (
         <DataTypeSelect
           label="Data Type"
           fieldName="general.xColumn.selectedDataType"
-          defaultValue={inferredXDataType}
+          defaultValue={inferredDataType}
         />
       )}
-      {shouldShowXTimeUnit && (
+      {isTemporalField(xColumn) && (
         <TimeUnitSelect
           fieldName="general.xColumn.timeUnit"
           label="Time Resolution"
         />
       )}
-      {xColumnExists && !isXCountField && (
+      {isNonCountField(xColumn) && (
         <>
           <SortField
             fieldName="general.xColumn.sort"
             label="Sort"
             defaultValue={formValues.general?.xColumn?.sort}
           />
-          <BinFields fieldName="xAxis" />
+          {isNumberField(xColumn) && <BinFields fieldName="xAxis" />}
         </>
       )}
     </FieldSection>
@@ -158,45 +214,26 @@ export const YAxis: React.FC = () => {
   const yColumnExists = isFieldSet(yColumn?.field);
   const xColumn = formValues.general?.xColumn;
   const xColumnExists = isFieldSet(xColumn?.field);
-
-  const inferredYDataType = yColumn?.type
-    ? convertDataTypeToSelectable(yColumn.type)
-    : "string";
-
-  const selectedYDataType = yColumn?.selectedDataType || inferredYDataType;
-  const isYCountField = yColumn?.field === COUNT_FIELD;
-
-  const shouldShowYAggregation =
-    yColumnExists && selectedYDataType !== "temporal" && !isYCountField;
-
-  const shouldShowYTimeUnit =
-    yColumnExists && selectedYDataType === "temporal" && !isYCountField;
+  const { inferredDataType } = getColumnDataTypes(yColumn);
 
   return (
     <FieldSection>
       <Title text="Y-Axis" />
-      <div className="flex flex-row justify-between">
-        <ColumnSelector
-          fieldName="general.yColumn.field"
-          columns={context.fields}
-        />
-        {shouldShowYAggregation && (
-          <AggregationSelect
-            fieldName="general.yColumn.aggregate"
-            selectedDataType={selectedYDataType}
-            binFieldName="yAxis.bin.binned"
-          />
-        )}
-      </div>
+      <ColumnSelectorWithAggregation
+        columnFieldName="general.yColumn.field"
+        column={yColumn}
+        columns={context.fields}
+        binFieldName="yAxis.bin.binned"
+      />
 
-      {yColumnExists && !isYCountField && (
+      {isNonCountField(yColumn) && (
         <DataTypeSelect
           label="Data Type"
           fieldName="general.yColumn.selectedDataType"
-          defaultValue={inferredYDataType}
+          defaultValue={inferredDataType}
         />
       )}
-      {shouldShowYTimeUnit && (
+      {isTemporalField(yColumn) && (
         <TimeUnitSelect
           fieldName="general.yColumn.timeUnit"
           label="Time Resolution"
@@ -205,6 +242,7 @@ export const YAxis: React.FC = () => {
       {yColumnExists && xColumnExists && (
         <BooleanField fieldName="general.horizontal" label="Invert axis" />
       )}
+      {isNumberField(yColumn) && <BinFields fieldName="yAxis" />}
     </FieldSection>
   );
 };
@@ -213,27 +251,20 @@ export const ColorByAxis: React.FC = () => {
   const { fields } = useChartFormContext();
   const form = useFormContext<z.infer<typeof ChartSchema>>();
   const formValues = useWatch({ control: form.control });
+  const colorByColumn = formValues.general?.colorByColumn;
 
-  let selectedColorByDataType =
-    formValues.general?.colorByColumn?.selectedDataType;
-  if (selectedColorByDataType === EMPTY_VALUE || !selectedColorByDataType) {
-    selectedColorByDataType = "string";
-  }
+  const showBinFields = isNumberField(colorByColumn);
 
   return (
     <FieldSection>
       <Title text="Color by" />
-      <div className="flex flex-row justify-between">
-        <ColumnSelector
-          fieldName="general.colorByColumn.field"
-          columns={fields}
-        />
-        <AggregationSelect
-          fieldName="general.colorByColumn.aggregate"
-          selectedDataType={selectedColorByDataType}
-          binFieldName="color.bin.binned"
-        />
-      </div>
+      <ColumnSelectorWithAggregation
+        columnFieldName="general.colorByColumn.field"
+        column={colorByColumn}
+        columns={fields}
+        binFieldName="color.bin.binned"
+      />
+      {showBinFields && <BinFields fieldName="color" />}
     </FieldSection>
   );
 };
@@ -248,14 +279,7 @@ export const Facet: React.FC = () => {
   const renderField = (facet: "column" | "row") => {
     const field = formValues.general?.facet?.[facet];
     const fieldExists = isFieldSet(field?.field);
-
-    const inferredDataType = field?.type
-      ? convertDataTypeToSelectable(field.type)
-      : "string";
-    const selectedDataType = field?.selectedDataType || inferredDataType;
-
-    const shouldShowTimeUnit = fieldExists && selectedDataType === "temporal";
-    const canShowBin = fieldExists && selectedDataType === "number";
+    const { inferredDataType } = getColumnDataTypes(field);
 
     const linkFieldName =
       facet === "row"
@@ -278,13 +302,13 @@ export const Facet: React.FC = () => {
               fieldName={`general.facet.${facet}.selectedDataType`}
               defaultValue={inferredDataType}
             />
-            {shouldShowTimeUnit && (
+            {isTemporalField(field) && (
               <TimeUnitSelect
                 fieldName={`general.facet.${facet}.timeUnit`}
                 label="Time Resolution"
               />
             )}
-            {canShowBin && (
+            {isNumberField(field) && (
               <div className="flex flex-row justify-between">
                 <BooleanField
                   fieldName={`general.facet.${facet}.binned`}
diff --git a/frontend/src/components/data-table/charts/components/form-fields.tsx b/frontend/src/components/data-table/charts/components/form-fields.tsx
index 0e4fc170..ff3e7725 100644
--- a/frontend/src/components/data-table/charts/components/form-fields.tsx
+++ b/frontend/src/components/data-table/charts/components/form-fields.tsx
@@ -65,7 +65,7 @@ import type { NumberFieldProps } from "@/components/ui/number-field";
 import { convertDataTypeToSelectable } from "../chart-spec/types";
 
 const CLEAR_VALUE = "__clear__";
-type FieldName = Path<z.infer<typeof ChartSchema>>;
+export type FieldName = Path<z.infer<typeof ChartSchema>>;
 
 export interface Field {
   name: string;
diff --git a/frontend/src/components/data-table/columns.tsx b/frontend/src/components/data-table/columns.tsx
index a11261bf..31a86060 100644
--- a/frontend/src/components/data-table/columns.tsx
+++ b/frontend/src/components/data-table/columns.tsx
@@ -31,7 +31,6 @@ import type { CalculateTopKRows } from "@/plugins/impl/DataTablePlugin";
 
 // Artificial limit to display long strings
 const MAX_STRING_LENGTH = 50;
-export const MAX_COLUMNS = 50;
 
 function inferDataType(value: unknown): [type: DataType, displayType: string] {
   if (typeof value === "string") {
diff --git a/frontend/src/components/editor/columns/cell-column.tsx b/frontend/src/components/editor/columns/cell-column.tsx
index 04b4c632..e37496ef 100644
--- a/frontend/src/components/editor/columns/cell-column.tsx
+++ b/frontend/src/components/editor/columns/cell-column.tsx
@@ -80,26 +80,22 @@ const ResizableComponent = ({
     onResize,
   });
 
-  const renderResizeHandler = (
-    ref: React.RefObject<HTMLDivElement>,
-    groupHover: boolean,
-  ) => {
-    const hoverClass = groupHover ? "group-hover" : "hover";
-
+  const renderResizeHandler = (ref: React.RefObject<HTMLDivElement>) => {
     return (
       <div
         ref={ref}
-        className={`w-1 cursor-col-resize transition-colors duration-200 z-10
-        relative before:content-[''] before:absolute before:inset-y-0 before:-left-[4px] before:right-[-4px] before:w-[11px] before:z-[-1]
-        ${hoverClass}/column:bg-[var(--slate-3)] dark:${hoverClass}/column:bg-[var(--slate-5)]
-        ${hoverClass}/column:hover:bg-primary/60 dark:${hoverClass}/column:hover:bg-primary/60`}
+        className={`w-[3px] cursor-col-resize transition-colors duration-200 z-[100]
+          relative before:content-[''] before:absolute before:inset-y-0 before:-left-[3px] 
+          before:right-[-3px] before:w-[9px] before:z-[-1]
+          hover/column:bg-[var(--slate-3)] dark:hover/column:bg-[var(--slate-5)]
+          hover/column:hover:bg-primary/60 dark:hover/column:hover:bg-primary/60`}
       />
     );
   };
 
   return (
     <div className="flex flex-row">
-      {renderResizeHandler(handleRefs.left, false)}
+      {renderResizeHandler(handleRefs.left)}
       <div
         ref={resizableDivRef}
         className="flex flex-col gap-5 box-content min-h-[100px] px-11 py-6 min-w-[500px] z-1"
@@ -107,7 +103,7 @@ const ResizableComponent = ({
       >
         {children}
       </div>
-      {renderResizeHandler(handleRefs.right, true)}
+      {renderResizeHandler(handleRefs.right)}
     </div>
   );
 };
diff --git a/frontend/src/components/slides/slides-component.tsx b/frontend/src/components/slides/slides-component.tsx
index 21d1256b..36239a0b 100644
--- a/frontend/src/components/slides/slides-component.tsx
+++ b/frontend/src/components/slides/slides-component.tsx
@@ -104,8 +104,7 @@ const SlidesComponent = ({
                 isFullscreen ? "p-20" : "p-6",
               )}
             >
-              {/* this centers the contents */}
-              <div className="m-auto max-w-full">{child}</div>
+              <div className="mo-slide-content">{child}</div>
             </div>
           </SwiperSlide>
         );
diff --git a/frontend/src/components/slides/slides.css b/frontend/src/components/slides/slides.css
index 7026b223..52e20e2f 100644
--- a/frontend/src/components/slides/slides.css
+++ b/frontend/src/components/slides/slides.css
@@ -56,3 +56,39 @@ so we are resetting this back to initial. */
 :host:host {
   display: initial;
 }
+
+/* The slides  is full width, but centers the inside content to fit the width of the content. */
+.mo-slide-content {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+
+  /* width is normally set to 100%, but we want the content to be centered around
+  the width needed. */
+  .output {
+    /* center the output vertically if there is space,
+    we use flexbox to center the output horizontally */
+    margin: auto 0;
+    max-width: 100%;
+    width: unset;
+  }
+
+  /* Components that should be full width when in slides mode */
+  div:has(> marimo-ui-element > marimo-vega) {
+    display: block !important;
+    flex: 1;
+
+    @media (min-width: 500px) {
+      min-width: 350px;
+    }
+  }
+}
+
+/* helpful for debugging */
+/* .mo-slide-content {
+  background-color: red;
+
+  .output {
+    background-color: green;
+  }
+} */
diff --git a/frontend/src/components/ui/range-slider.tsx b/frontend/src/components/ui/range-slider.tsx
index ab385aaf..843f718c 100644
--- a/frontend/src/components/ui/range-slider.tsx
+++ b/frontend/src/components/ui/range-slider.tsx
@@ -28,6 +28,7 @@ const RangeSlider = React.forwardRef<
         "relative flex touch-none select-none hover:cursor-pointer",
         "data-[orientation=horizontal]:w-full data-[orientation=horizontal]:items-center",
         "data-[orientation=vertical]:h-full data-[orientation=vertical]:justify-center",
+        "data-[disabled]:cursor-not-allowed",
         className,
       )}
       {...props}
@@ -46,6 +47,7 @@ const RangeSlider = React.forwardRef<
             "absolute bg-blue-500 dark:bg-primary",
             "data-[orientation=horizontal]:h-full",
             "data-[orientation=vertical]:w-full",
+            "data-[disabled]:opacity-50",
           )}
         />
       </SliderPrimitive.Track>
@@ -54,7 +56,7 @@ const RangeSlider = React.forwardRef<
           <TooltipTrigger asChild={true}>
             <SliderPrimitive.Thumb
               data-testid="thumb"
-              className="block h-4 w-4 rounded-full shadow-xsSolid border border-blue-500 dark:border-primary dark:bg-accent bg-white hover:bg-blue-300 focus:bg-blue-300 transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50"
+              className="block h-4 w-4 rounded-full shadow-xsSolid border border-blue-500 dark:border-primary dark:bg-accent bg-white hover:bg-blue-300 focus:bg-blue-300 transition-colors focus-visible:outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
               onFocus={openActions.setTrue}
               onBlur={openActions.setFalse}
               onMouseEnter={openActions.setTrue}
@@ -75,7 +77,7 @@ const RangeSlider = React.forwardRef<
           <TooltipTrigger asChild={true}>
             <SliderPrimitive.Thumb
               data-testid="thumb"
-              className="block h-4 w-4 rounded-full shadow-xsSolid border border-blue-500 dark:border-primary dark:bg-accent bg-white hover:bg-blue-300 focus:bg-blue-300 transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50"
+              className="block h-4 w-4 rounded-full shadow-xsSolid border border-blue-500 dark:border-primary dark:bg-accent bg-white hover:bg-blue-300 focus:bg-blue-300 transition-colors focus-visible:outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
               onFocus={openActions.setTrue}
               onBlur={openActions.setFalse}
               onMouseEnter={openActions.setTrue}
diff --git a/frontend/src/plugins/impl/DataTablePlugin.tsx b/frontend/src/plugins/impl/DataTablePlugin.tsx
index 42c5ef67..907d5576 100644
--- a/frontend/src/plugins/impl/DataTablePlugin.tsx
+++ b/frontend/src/plugins/impl/DataTablePlugin.tsx
@@ -5,7 +5,6 @@ import { DataTable } from "../../components/data-table/data-table";
 import {
   generateColumns,
   inferFieldTypes,
-  MAX_COLUMNS,
 } from "../../components/data-table/columns";
 import { Labeled } from "./common/labeled";
 import { Alert, AlertTitle } from "@/components/ui/alert";
@@ -116,6 +115,7 @@ interface Data<T> {
   textJustifyColumns?: Record<string, "left" | "center" | "right">;
   wrappedColumns?: string[];
   totalColumns: number;
+  maxColumns: number | "all";
   hasStableRowId: boolean;
   lazy: boolean;
 }
@@ -183,6 +183,7 @@ export const DataTablePlugin = createPlugin<S>("marimo-table")
         )
         .nullish(),
       totalColumns: z.number(),
+      maxColumns: z.union([z.number(), z.literal("all")]),
       hasStableRowId: z.boolean().default(false),
       cellStyles: z.record(z.record(z.object({}).passthrough())).optional(),
       // Whether to load the data lazily.
@@ -606,6 +607,7 @@ const DataTableComponent = ({
   label,
   data,
   totalRows,
+  maxColumns,
   pagination,
   selection,
   value,
@@ -674,10 +676,13 @@ const DataTableComponent = ({
 
   const memoizedUnclampedFieldTypes =
     useDeepCompareMemoize(fieldTypesOrInferred);
-  const memoizedClampedFieldTypes = useMemo(
-    () => memoizedUnclampedFieldTypes.slice(0, MAX_COLUMNS),
-    [memoizedUnclampedFieldTypes],
-  );
+
+  const memoizedClampedFieldTypes = useMemo(() => {
+    if (maxColumns === "all") {
+      return memoizedUnclampedFieldTypes;
+    }
+    return memoizedUnclampedFieldTypes.slice(0, maxColumns);
+  }, [maxColumns, memoizedUnclampedFieldTypes]);
 
   const memoizedRowHeaders = useDeepCompareMemoize(rowHeaders);
   const memoizedTextJustifyColumns = useDeepCompareMemoize(textJustifyColumns);
diff --git a/frontend/src/plugins/impl/RangeSliderPlugin.tsx b/frontend/src/plugins/impl/RangeSliderPlugin.tsx
index bf7052f0..88864076 100644
--- a/frontend/src/plugins/impl/RangeSliderPlugin.tsx
+++ b/frontend/src/plugins/impl/RangeSliderPlugin.tsx
@@ -21,6 +21,7 @@ interface Data {
   orientation: "horizontal" | "vertical";
   showValue: boolean;
   fullWidth: boolean;
+  disabled?: boolean;
 }
 
 export class RangeSliderPlugin implements IPlugin<T, Data> {
@@ -37,6 +38,7 @@ export class RangeSliderPlugin implements IPlugin<T, Data> {
     orientation: z.enum(["horizontal", "vertical"]).default("horizontal"),
     showValue: z.boolean().default(false),
     fullWidth: z.boolean().default(false),
+    disabled: z.boolean().optional(),
   });
 
   render(props: IPluginProps<T, Data>): JSX.Element {
@@ -76,6 +78,7 @@ const RangeSliderComponent = ({
   orientation,
   showValue,
   fullWidth,
+  disabled,
   valueMap,
 }: RangeSliderProps): JSX.Element => {
   const id = useId();
@@ -116,6 +119,7 @@ const RangeSliderComponent = ({
           max={stop}
           step={step}
           orientation={orientation}
+          disabled={disabled}
           // Triggered on all value changes
           onValueChange={(nextValue: number[]) => {
             setInternalValue(nextValue);
diff --git a/frontend/src/plugins/impl/data-frames/DataFramePlugin.tsx b/frontend/src/plugins/impl/data-frames/DataFramePlugin.tsx
index 62bd63b6..d9349fe7 100644
--- a/frontend/src/plugins/impl/data-frames/DataFramePlugin.tsx
+++ b/frontend/src/plugins/impl/data-frames/DataFramePlugin.tsx
@@ -267,6 +267,7 @@ export const DataFrameComponent = memo(
           hasStableRowId={false}
           totalRows={total_rows ?? 0}
           totalColumns={Object.keys(columns).length}
+          maxColumns="all"
           pageSize={pageSize}
           pagination={true}
           fieldTypes={field_types}
diff --git a/marimo/__init__.py b/marimo/__init__.py
index 9be1936e..3e6861b9 100644
--- a/marimo/__init__.py
+++ b/marimo/__init__.py
@@ -84,7 +84,7 @@ __all__ = [
     "vstack",
     "watch",
 ]
-__version__ = "0.13.9"
+__version__ = "0.13.10"
 
 import marimo._ai as ai
 import marimo._islands as islands
diff --git a/marimo/_plugins/ui/_impl/input.py b/marimo/_plugins/ui/_impl/input.py
index d93494e7..4cca8e0f 100644
--- a/marimo/_plugins/ui/_impl/input.py
+++ b/marimo/_plugins/ui/_impl/input.py
@@ -416,6 +416,7 @@ class range_slider(UIElement[list[Numeric], Sequence[Numeric]]):
         label (str): Markdown label for the element.
         on_change (Optional[Callable[[Sequence[Numeric]], None]]): Optional callback to run when this element's value changes.
         full_width (bool): Whether the input should take up the full width of its container.
+        disabled (bool, optional): Whether the slider is disabled. Defaults to False.
 
     Methods:
         from_series(series: DataFrameSeries, **kwargs: Any) -> range_slider:
@@ -439,6 +440,7 @@ class range_slider(UIElement[list[Numeric], Sequence[Numeric]]):
         label: str = "",
         on_change: Optional[Callable[[Sequence[Numeric]], None]] = None,
         full_width: bool = False,
+        disabled: bool = False,
     ) -> None:
         self.start: Numeric
         self.stop: Numeric
@@ -504,6 +506,7 @@ class range_slider(UIElement[list[Numeric], Sequence[Numeric]]):
                     "orientation": orientation,
                     "show-value": show_value,
                     "full-width": full_width,
+                    "disabled": disabled,
                 },
                 on_change=on_change,
             )
@@ -544,6 +547,7 @@ class range_slider(UIElement[list[Numeric], Sequence[Numeric]]):
                     "orientation": orientation,
                     "show-value": show_value,
                     "full-width": full_width,
+                    "disabled": disabled,
                 },
                 on_change=on_change,
             )
diff --git a/marimo/_plugins/ui/_impl/table.py b/marimo/_plugins/ui/_impl/table.py
index 7a38b5bd..d4e975b4 100644
--- a/marimo/_plugins/ui/_impl/table.py
+++ b/marimo/_plugins/ui/_impl/table.py
@@ -99,8 +99,8 @@ class ColumnSummaries:
 
 DEFAULT_MAX_COLUMNS = 50
 
-MaxColumnsNotProvided = Literal["max_columns_not_provided"]
-MAX_COLUMNS_NOT_PROVIDED: MaxColumnsNotProvided = "max_columns_not_provided"
+MaxColumnsNotProvided = Literal["inherit"]
+MAX_COLUMNS_NOT_PROVIDED: MaxColumnsNotProvided = "inherit"
 
 
 @dataclass(frozen=True)
@@ -440,6 +440,7 @@ class table(
         # Holds the original data
         self._manager = get_table_manager(data)
         self._max_columns = max_columns
+        max_columns_arg = "all" if max_columns is None else max_columns
 
         if _internal_total_rows is not None:
             total_rows = _internal_total_rows
@@ -595,6 +596,7 @@ class table(
                 "data": search_result_data,
                 "total-rows": total_rows,
                 "total-columns": num_columns,
+                "max-columns": max_columns_arg,
                 "banner-text": self._get_banner_text(),
                 "pagination": pagination,
                 "page-size": page_size,
@@ -960,8 +962,8 @@ class table(
                 - sort: Optional sorting configuration
                 - filters: Optional list of filter conditions
                 - limit: Optional row limit
-                - max_columns: Optional max number of columns to display. If
-                  `MAX_COLUMNS_NOT_PROVIDED`, default to table's max columns.
+                - max_columns: Optional max number of columns. None means show all columns,
+                  MAX_COLUMNS_NOT_PROVIDED means use the table's max_columns setting.
 
         Returns:
             SearchTableResponse: Response containing:
diff --git a/pyproject.toml b/pyproject.toml
index 16e2e190..b78aad2b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,8 @@ dynamic = ["version"]
 # Dependencies should have lower bounds, which should be as loose as possible.
 dependencies = [
     # For maintainable cli
-    "click>=8.0,<9",
+    # - 8.2.0 introduced a bug where boolean flags broke
+    "click>=8.0,!=8.2.0,<9",
     # code completion
     "jedi>=0.18.0",
     # compile markdown to html


Test patch:
diff --git a/tests/_plugins/ui/_impl/test_table.py b/tests/_plugins/ui/_impl/test_table.py
index c5f0d710..d71a9ff8 100644
--- a/tests/_plugins/ui/_impl/test_table.py
+++ b/tests/_plugins/ui/_impl/test_table.py
@@ -11,6 +11,8 @@ from marimo._dependencies.dependencies import DependencyManager
 from marimo._plugins import ui
 from marimo._plugins.ui._impl.dataframes.transforms.types import Condition
 from marimo._plugins.ui._impl.table import (
+    DEFAULT_MAX_COLUMNS,
+    MAX_COLUMNS_NOT_PROVIDED,
     CalculateTopKRowsArgs,
     CalculateTopKRowsResponse,
     DownloadAsArgs,
@@ -1283,6 +1285,7 @@ def test_column_clamping():
     table = ui.table(data)
     assert len(table._manager.get_column_names()) == 100
     assert table._component_args["total-columns"] == 100
+    assert table._component_args["max-columns"] == DEFAULT_MAX_COLUMNS
     assert len(json.loads(table._component_args["data"])[0].keys()) == 50
     assert table._component_args["field-types"] is None
 
@@ -1290,6 +1293,7 @@ def test_column_clamping():
     table = ui.table(data, max_columns=20)
     assert len(table._manager.get_column_names()) == 100
     assert table._component_args["total-columns"] == 100
+    assert table._component_args["max-columns"] == 20
     assert len(json.loads(table._component_args["data"])[0].keys()) == 20
     assert table._component_args["field-types"] is None
 
@@ -1297,6 +1301,7 @@ def test_column_clamping():
     table = ui.table(data, max_columns=None)
     assert len(table._manager.get_column_names()) == 100
     assert table._component_args["total-columns"] == 100
+    assert table._component_args["max-columns"] == "all"
     assert len(json.loads(table._component_args["data"])[0].keys()) == 100
     assert table._component_args["field-types"] is None
 
@@ -1308,6 +1313,7 @@ def test_column_clamping_with_small_data():
     table = ui.table(data)
     assert len(table._manager.get_column_names()) == 10
     assert table._component_args["total-columns"] == 10
+    assert table._component_args["max-columns"] == DEFAULT_MAX_COLUMNS
     assert len(json.loads(table._component_args["data"])[0].keys()) == 10
     assert table._component_args["field-types"] is None
 
@@ -1374,6 +1380,7 @@ def test_column_clamping_with_exact_max_columns():
     # Check that the table is not clamped
     assert len(table._manager.get_column_names()) == 50
     assert table._component_args["total-columns"] == 50
+    assert table._component_args["max-columns"] == DEFAULT_MAX_COLUMNS
     assert len(json.loads(table._component_args["data"])[0].keys()) == 50
     assert table._component_args["field-types"] is None
 
@@ -1385,6 +1392,7 @@ def test_column_clamping_with_more_than_max_columns():
     # Check that the table is clamped
     assert len(table._manager.get_column_names()) == 60
     assert table._component_args["total-columns"] == 60
+    assert table._component_args["max-columns"] == DEFAULT_MAX_COLUMNS
     assert len(json.loads(table._component_args["data"])[0].keys()) == 50
     assert table._component_args["field-types"] is None
 
@@ -1395,6 +1403,7 @@ def test_column_clamping_with_no_columns():
     # Check that the table handles no columns gracefully
     assert len(table._manager.get_column_names()) == 1
     assert table._component_args["total-columns"] == 1
+    assert table._component_args["max-columns"] == DEFAULT_MAX_COLUMNS
     assert len(json.loads(table._component_args["data"])) == 0
     assert table._component_args["field-types"] is None
 
@@ -1406,6 +1415,7 @@ def test_column_clamping_with_single_column():
     # Check that the table handles a single column gracefully
     assert len(table._manager.get_column_names()) == 1
     assert table._component_args["total-columns"] == 1
+    assert table._component_args["max-columns"] == DEFAULT_MAX_COLUMNS
     assert len(json.loads(table._component_args["data"])[0].keys()) == 1
     assert table._component_args["field-types"] is None
 
@@ -1422,6 +1432,7 @@ def test_column_clamping_with_polars():
     # Check that the table is clamped
     assert len(table._manager.get_column_names()) == 60
     assert table._component_args["total-columns"] == 60
+    assert table._component_args["max-columns"] == DEFAULT_MAX_COLUMNS
     json_data = json.loads(table._component_args["data"])
     headers = json_data[0].keys()
     assert len(headers) == 50  # 50 columns
@@ -1433,6 +1444,7 @@ def test_column_clamping_with_polars():
     # Check that the table is clamped
     assert len(table._manager.get_column_names()) == 60
     assert table._component_args["total-columns"] == 60
+    assert table._component_args["max-columns"] == 40
     json_data = json.loads(table._component_args["data"])
     headers = json_data[0].keys()
     assert len(headers) == 40  # 40 columns
@@ -1444,6 +1456,7 @@ def test_column_clamping_with_polars():
     # Check that the table is not clamped
     assert len(table._manager.get_column_names()) == 60
     assert table._component_args["total-columns"] == 60
+    assert table._component_args["max-columns"] == "all"
     json_data = json.loads(table._component_args["data"])
     headers = json_data[0].keys()
 
@@ -1469,6 +1482,7 @@ def test_dataframe_with_int_column_names():
     # Check that the table handles integer column names correctly
     assert table._manager.get_column_names() == [0, 1, 2]
     assert table._component_args["total-columns"] == 3
+    assert table._component_args["max-columns"] == DEFAULT_MAX_COLUMNS
 
 
 def test_cell_initial_style():
@@ -1640,6 +1654,7 @@ def test_lazy_dataframe() -> None:
         assert table._component_args["pagination"] is False
         assert table._component_args["data"] == []
         assert table._component_args["total-columns"] == 0
+        assert table._component_args["max-columns"] == DEFAULT_MAX_COLUMNS
         assert table._component_args["field-types"] is None
 
         # Verify that search response indicates "too_many" for total_rows
@@ -1763,3 +1778,121 @@ def _convert_data_bytes_to_pandas_df(
         return pd.read_parquet(io.BytesIO(data_bytes))
     else:
         raise ValueError(f"Unsupported data_format: {data_format}")
+
+
+def test_max_columns_not_provided():
+    # Create data with many columns
+    data = {f"col{i}": [1, 2, 3] for i in range(100)}
+    table = ui.table(data)
+
+    # Test default behavior (should use DEFAULT_MAX_COLUMNS=50)
+    search_args = SearchTableArgs(
+        page_size=10, page_number=0, max_columns=MAX_COLUMNS_NOT_PROVIDED
+    )
+    response = table._search(search_args)
+    result_data = json.loads(response.data)
+    assert len(result_data[0].keys()) == 50
+
+    # Test when not set (uses MAX_COLUMNS_NOT_PROVIDED as the default)
+    search_args = SearchTableArgs(page_size=10, page_number=0)
+    response = table._search(search_args)
+    result_data = json.loads(response.data)
+    assert len(result_data[0].keys()) == 50
+
+    # Test with explicit max_columns
+    search_args = SearchTableArgs(page_size=10, page_number=0, max_columns=20)
+    response = table._search(search_args)
+    result_data = json.loads(response.data)
+    assert len(result_data[0].keys()) == 20
+
+    # Test with max_columns=None (show all columns)
+    search_args = SearchTableArgs(
+        page_size=10, page_number=0, max_columns=None
+    )
+    response = table._search(search_args)
+    result_data = json.loads(response.data)
+    assert len(result_data[0].keys()) == 100
+
+
+def test_max_columns_not_provided_with_sort():
+    # Create data with many columns
+    data = {f"col{i}": [1, 2, 3] for i in range(100)}
+    table = ui.table(data)
+
+    # Test sort with default max_columns
+    search_args = SearchTableArgs(
+        page_size=10,
+        page_number=0,
+        sort=SortArgs(by="col0", descending=True),
+        max_columns=MAX_COLUMNS_NOT_PROVIDED,
+    )
+    response = table._search(search_args)
+    result_data = json.loads(response.data)
+    assert len(result_data[0].keys()) == 50
+
+    # Test sort with explicit max_columns
+    search_args = SearchTableArgs(
+        page_size=10,
+        page_number=0,
+        sort=SortArgs(by="col0", descending=True),
+        max_columns=20,
+    )
+    response = table._search(search_args)
+    result_data = json.loads(response.data)
+    assert len(result_data[0].keys()) == 20
+
+    # Test sort with max_columns=None
+    search_args = SearchTableArgs(
+        page_size=10,
+        page_number=0,
+        sort=SortArgs(by="col0", descending=True),
+        max_columns=None,
+    )
+    response = table._search(search_args)
+    result_data = json.loads(response.data)
+    assert len(result_data[0].keys()) == 100
+
+
+@pytest.mark.skipif(
+    not DependencyManager.polars.has(),
+    reason="Pandas not installed",
+)
+def test_max_columns_not_provided_with_filters():
+    # Create data with many columns
+    import polars as pl
+
+    data = pl.DataFrame({f"col{i}": [1, 2, 3] for i in range(100)})
+    table = ui.table(data)
+
+    # Test filters with default max_columns
+    search_args = SearchTableArgs(
+        page_size=10,
+        page_number=0,
+        filters=[Condition(column_id="col0", operator="==", value=1)],
+        max_columns=MAX_COLUMNS_NOT_PROVIDED,
+    )
+    response = table._search(search_args)
+    result_data = json.loads(response.data)
+    assert len(result_data[0].keys()) == 50
+
+    # Test filters with explicit max_columns
+    search_args = SearchTableArgs(
+        page_size=10,
+        page_number=0,
+        filters=[Condition(column_id="col0", operator="==", value=1)],
+        max_columns=20,
+    )
+    response = table._search(search_args)
+    result_data = json.loads(response.data)
+    assert len(result_data[0].keys()) == 20
+
+    # Test filters with max_columns=None
+    search_args = SearchTableArgs(
+        page_size=10,
+        page_number=0,
+        filters=[Condition(column_id="col0", operator="==", value=1)],
+        max_columns=None,
+    )
+    response = table._search(search_args)
+    result_data = json.loads(response.data)
+    assert len(result_data[0].keys()) == 101  # +1 for marimo_row_id