Auto-Expand Columns in React Tables: The Hidden Complexity Behind Seamless UX

Auto-ExpandTutorialAdvanced

Making table columns automatically fill the screen seems simple—until users resize them. Discover the sophisticated algorithms behind auto-expanding columns and why this "simple" feature is one of the hardest problems in data grid engineering.

You're building a dashboard. The table has 5 columns, and you want them to fill the entire container width—no horizontal scrolling, no wasted space. Just columns that expand proportionally to use 100% of the available width. Sounds simple, right?

Then a user resizes one column. Now what? If they make the "Name" column wider, which other columns should shrink? By how much? What if those columns hit their minimum width? What if you have pinned columns on the left and right? What if the user is on a phone where the sum of all minimum widths exceeds the screen size?

Welcome to autoExpandColumns—one of the most deceptively complex features in modern data grids. What looks like "just scale everything proportionally" requires sophisticated algorithms handling multiple coordinate systems, proportional distribution, hierarchical structures, and countless edge cases—all to deliver an intuitive Excel-like experience where everything just works.

Why Auto-Expand Columns Is Hard

The challenge isn't the initial scaling—that's straightforward math. The real complexity emerges when users interact with the table:

Challenge #1: Which Columns Compensate?

When a user makes one column wider, the total width must stay constant (to fill the container). Other columns need to shrink. But which ones?

The Directional Logic Problem

  • Leftmost column: Growing it should shrink columns to the right
  • Rightmost column: Growing it should shrink columns to the left
  • Middle columns: Which direction? Left or right?
  • Right-pinned columns: They grow leftward (opposite mouse direction!)

Challenge #2: Proportional Compensation

You can't just shrink one column. That feels arbitrary and breaks user expectations. Instead, you need to distribute the space change proportionally across multiple columns based on their current widths.

React TSX
1// Example: User grows "Name" column by 100px
2// Container width: 1000px (must stay constant)
3// Other columns: "Email" (300px), "Role" (200px), "Status" (100px)
4
5// Naive approach: Shrink all equally by 33.3px each
6// ❌ Problem: Status (100px) shrinks by 33%, but Email (300px) only shrinks by 11%
7// This feels wrong—larger columns should absorb more change
8
9// Proportional approach: Shrink based on current width
10// Total width of other columns: 600px
11// Email shrinks: 100px * (300/600) = 50px → new width: 250px
12// Role shrinks: 100px * (200/600) = 33.3px → new width: 166.7px
13// Status shrinks: 100px * (100/600) = 16.7px → new width: 83.3px
14// ✅ Feels natural—larger columns absorb more change

Challenge #3: Minimum Width Constraints

Columns have minimum widths (typically 30px) to prevent them from becoming unusable. When a column hits its minimum during proportional shrinking, you need to:

  1. Stop shrinking that column
  2. Redistribute its "share" of the shrinkage to other columns
  3. Repeat iteratively until all columns are either at minimum or properly sized
React TSX
1// Iterative algorithm for minimum width constraints
2function distributeWidthChange(columns, totalChange) {
3 let remainingChange = totalChange;
4 let activeColumns = [...columns];
5
6 while (remainingChange > 0 && activeColumns.length > 0) {
7 // Calculate proportional share for each active column
8 const totalActiveWidth = activeColumns.reduce((sum, col) => sum + col.width, 0);
9
10 for (const column of activeColumns) {
11 const proportionalChange = remainingChange * (column.width / totalActiveWidth);
12 const newWidth = column.width - proportionalChange;
13
14 if (newWidth < column.minWidth) {
15 // Column hit minimum—lock it and redistribute its share
16 const actualChange = column.width - column.minWidth;
17 column.width = column.minWidth;
18 remainingChange -= actualChange;
19 activeColumns = activeColumns.filter(c => c !== column);
20 } else {
21 column.width = newWidth;
22 remainingChange -= proportionalChange;
23 }
24 }
25 }
26}

Challenge #4: Pinned Columns Create Isolated Sections

When you have pinned columns, the table splits into three independent sections: left-pinned, main (scrollable), and right-pinned. Resizing a column only affects other columns in the same section.

Section Isolation

Each section maintains its own width constraints independently:

  • Left-pinned section: Fixed total width, columns compensate within this section
  • Main section: Flexible width, fills remaining space
  • Right-pinned section: Fixed total width, columns compensate within this section

Challenge #5: Nested Headers

When you have nested headers (parent headers spanning multiple child columns), resizing a parent should resize all its children proportionally. The system must:

  • Always work with leaf headers (actual columns) as the source of truth
  • Calculate scale factors to resize all children proportionally
  • Determine compensation direction based on the children's position range

Challenge #6: Mobile & Responsive Constraints

On small screens, the sum of column minimum widths might exceed the container width. What happens when it's mathematically impossible to fit all columns?

The Mobile Dilemma

On a 375px phone screen with 5 columns at 80px minimum each:

  • Required width: 5 × 80px = 400px
  • Available width: 375px
  • Deficit: 25px

Solution: Pinned sections are limited to 30% (mobile), 40% (tablet), or 80% (desktop) of container width, preventing them from dominating small screens.

How Auto-Expand Columns Works Internally

Let's break down the sophisticated algorithm that makes auto-expanding columns feel seamless:

Step 1: Initial Scaling on Mount

When the table first renders, it calculates a scale factor to fill the container:

React TSX
1// Initial scaling algorithm
2function scaleColumnsToFit(columns, containerWidth) {
3 // Sum all column widths
4 const totalWidth = columns.reduce((sum, col) => sum + col.width, 0);
5
6 // Calculate scale factor
7 const scaleFactor = containerWidth / totalWidth;
8
9 // Apply scale to all columns proportionally
10 return columns.map(col => ({
11 ...col,
12 width: col.width * scaleFactor
13 }));
14}
15
16// Example:
17// Container: 1000px
18// Columns: [200px, 300px, 150px, 250px] = 900px total
19// Scale factor: 1000 / 900 = 1.111
20// New widths: [222px, 333px, 167px, 278px] = 1000px ✓

Important: minWidth NOT Enforced During Initial Scaling

During initial scaling, columns can be scaled below their minWidth. This ensures the table always fills the container, even on small screens. The minWidth is only enforced during user resizing.

Step 2: Capturing Initial Widths on Resize Start

When a user starts resizing, the system captures all current column widths. This prevents cascading rounding errors during the resize operation:

React TSX
1// Capture widths at drag start
2function onResizeStart(columnId) {
3 // Store initial widths for all columns
4 const initialWidths = columns.map(col => ({
5 id: col.id,
6 width: col.currentWidth // Current rendered width
7 }));
8
9 // Use these throughout the resize operation
10 // This prevents: 100 → 99.7 → 99.4 → 99.1 (rounding drift)
11 // Instead: Always calculate from original 100
12}

Step 3: Proportional Compensation Algorithm

As the user drags, the system calculates which columns should compensate and by how much:

React TSX
1function compensateColumns(resizedColumn, widthDelta, allColumns) {
2 // 1. Determine direction (left or right)
3 const direction = getCompensationDirection(resizedColumn);
4
5 // 2. Get columns that should compensate
6 const columnsToCompensate = getColumnsInDirection(resizedColumn, direction);
7
8 // 3. Calculate total width of compensating columns
9 const totalCompensateWidth = columnsToCompensate.reduce(
10 (sum, col) => sum + col.initialWidth,
11 0
12 );
13
14 // 4. Distribute change proportionally
15 const newWidths = {};
16 let remainingDelta = widthDelta;
17
18 for (const column of columnsToCompensate) {
19 // Proportional share based on initial width
20 const proportion = column.initialWidth / totalCompensateWidth;
21 const change = widthDelta * proportion;
22 const newWidth = column.initialWidth - change; // Subtract (compensate)
23
24 // Check minimum width constraint
25 if (newWidth < column.minWidth) {
26 newWidths[column.id] = column.minWidth;
27 remainingDelta -= (column.initialWidth - column.minWidth);
28 } else {
29 newWidths[column.id] = newWidth;
30 remainingDelta -= change;
31 }
32 }
33
34 // 5. If columns hit minimum, redistribute remaining delta
35 if (remainingDelta > 0) {
36 redistributeRemaining(newWidths, remainingDelta, columnsToCompensate);
37 }
38
39 return newWidths;
40}

Step 4: Smart Directional Logic

React TSX
1function getCompensationDirection(column) {
2 // Leftmost column in section → compensate right
3 if (column.isLeftmost) {
4 return 'right';
5 }
6
7 // Rightmost column in section → compensate left
8 if (column.isRightmost) {
9 return 'left';
10 }
11
12 // Right-pinned columns → compensate left (they grow leftward)
13 if (column.section === 'right-pinned') {
14 return 'left';
15 }
16
17 // Middle columns in main/left-pinned → compensate right
18 return 'right';
19}

Step 5: Handling Right-Pinned Columns

Right-pinned columns are tricky because they grow leftward (opposite to the mouse movement). The system inverts the mouse delta for these columns:

React TSX
1function calculateWidthDelta(mouseDelta, column) {
2 if (column.section === 'right-pinned') {
3 // Right-pinned: growing right (positive mouse delta)
4 // means column should shrink (negative width delta)
5 return -mouseDelta;
6 }
7
8 // Normal columns: positive mouse delta = grow
9 return mouseDelta;
10}

How to Use Auto-Expand Columns in Simple Table

Despite the complexity under the hood, using auto-expand columns is incredibly simple. Just add one prop:

Basic Example

React TSX
1import { SimpleTable, HeaderObject } from "simple-table-core";
2import "simple-table-core/styles.css";
3
4const headers: HeaderObject[] = [
5 {
6 accessor: "id",
7 label: "ID",
8 width: 80,
9 type: "number",
10 },
11 {
12 accessor: "name",
13 label: "Name",
14 width: 200,
15 type: "string",
16 },
17 {
18 accessor: "email",
19 label: "Email",
20 width: 250,
21 type: "string",
22 },
23 {
24 accessor: "role",
25 label: "Role",
26 width: 150,
27 type: "string",
28 },
29 {
30 accessor: "status",
31 label: "Status",
32 width: 120,
33 type: "string",
34 },
35];
36
37export default function AutoExpandTable({ data }) {
38 return (
39 <SimpleTable
40 defaultHeaders={headers}
41 rows={data}
42 rowIdAccessor="id"
43 autoExpandColumns={true} // That's it!
44 height="500px"
45 />
46 );
47}

What Happens

  • Columns scale proportionally to fill the container width (no horizontal scroll)
  • The width values are used as the base for proportional distribution
  • When users resize columns, other columns compensate automatically
  • All the complex algorithms run invisibly in the background

📱 Mobile Recommendation

It's recommended to not use auto-expand columns on mobile devices. On small screens, horizontal scrolling often provides a better user experience than cramped, auto-scaled columns.

React TSX
1const [isMobile, setIsMobile] = useState(false);
2
3useEffect(() => {
4 const checkMobile = () => {
5 setIsMobile(window.innerWidth < 768);
6 };
7 checkMobile();
8 window.addEventListener("resize", checkMobile);
9 return () => window.removeEventListener("resize", checkMobile);
10}, []);
11
12<SimpleTable
13 autoExpandColumns={!isMobile}
14 defaultHeaders={headers}
15 rows={data}
16 rowIdAccessor="id"
17/>

With Pinned Columns

Auto-expand works seamlessly with pinned columns. Each section (left-pinned, main, right-pinned) maintains its own proportional scaling:

React TSX
1const headers: HeaderObject[] = [
2 {
3 accessor: "id",
4 label: "ID",
5 width: 60,
6 pinned: "left", // Left-pinned section
7 },
8 {
9 accessor: "name",
10 label: "Name",
11 width: 180,
12 pinned: "left", // Left-pinned section
13 },
14 {
15 accessor: "email",
16 label: "Email",
17 width: 250, // Main section
18 },
19 {
20 accessor: "department",
21 label: "Department",
22 width: 180, // Main section
23 },
24 {
25 accessor: "salary",
26 label: "Salary",
27 width: 140, // Main section
28 },
29 {
30 accessor: "actions",
31 label: "Actions",
32 width: 120,
33 pinned: "right", // Right-pinned section
34 },
35];
36
37<SimpleTable
38 defaultHeaders={headers}
39 rows={data}
40 rowIdAccessor="id"
41 autoExpandColumns={true}
42 height="500px"
43/>

Section Behavior

  • Left-pinned: ID + Name scale to fill their section
  • Main: Email + Department + Salary scale to fill remaining space
  • Right-pinned: Actions column maintains its width
  • Resizing a column only affects other columns in the same section

With Column Resizing

Enable column resizing to let users adjust widths. Auto-expand ensures the table always fills the container:

React TSX
1<SimpleTable
2 defaultHeaders={headers}
3 rows={data}
4 rowIdAccessor="id"
5 autoExpandColumns={true}
6 isColumnResizable={true} // Enable resizing
7 height="500px"
8/>
9
10// Users can now:
11// 1. Drag column borders to resize
12// 2. Other columns automatically compensate
13// 3. Table always fills 100% of container width
14// 4. Minimum widths are respected (default 30px)

When to Use Auto-Expand Columns

Perfect Use Cases

  • Dashboards: Tables that should fill a card or panel completely
  • Reports: Financial or analytics tables where horizontal scrolling is undesirable
  • Admin panels: Full-width tables with 4-8 columns that fit comfortably
  • Responsive layouts: Tables that need to adapt to any container size
  • Fixed-height containers: When the table is the primary content

When to Avoid

  • Many columns (15+): Horizontal scrolling is more natural than tiny columns
  • Variable content width: When some columns need specific widths (e.g., timestamps)
  • Dense data: Spreadsheet-like tables where users expect to scroll horizontally
  • Mobile devices: On small screens (< 768px), horizontal scrolling provides better UX than cramped columns. Disable autoExpandColumns on mobile.

Auto-Expand vs. Fixed Width vs. "1fr"

ApproachBehaviorBest For
autoExpandColumnsAll columns scale proportionally to fill container. User resizing adjusts other columns.Dashboards, reports, full-width layouts
Fixed WidthEach column has exact pixel width. Horizontal scroll if total exceeds container.Dense data, many columns, precise control
width: "1fr"Specific columns share available space equally. Mix with fixed-width columns.Hybrid layouts: some fixed, some flexible

Mobile & Responsive Considerations

📱 Recommended: Disable on Mobile

On mobile devices, it's recommended to set autoExpandColumns=. Small screens benefit more from horizontal scrolling than cramped, auto-scaled columns. This gives users better control and readability.

React TSX
1const [isMobile, setIsMobile] = useState(false);
2
3useEffect(() => {
4 const checkMobile = () => {
5 setIsMobile(window.innerWidth < 768);
6 };
7 checkMobile();
8 window.addEventListener("resize", checkMobile);
9 return () => window.removeEventListener("resize", checkMobile);
10}, []);
11
12<SimpleTable
13 autoExpandColumns={!isMobile} // Disable on mobile
14 defaultHeaders={headers}
15 rows={data}
16 rowIdAccessor="id"
17/>

Performance Considerations

Auto-expand columns require real-time calculations during user interactions. Here's how Simple Table keeps it performant:

⚡ Efficient Calculations

Width calculations are O(n) where n = number of columns in the section. Even with 50 columns, this is negligible. Calculations happen only during resize, not on every render.

🎯 Paused During Active Resize

When a user is actively resizing, the initial scaling algorithm is paused to avoid conflicts. It resumes after the resize completes.

🔢 No Cascading Rounding Errors

By capturing initial widths at drag start and always calculating from those values, the system avoids cumulative rounding errors that would cause columns to drift over time.

📱 Responsive Constraints

Pinned sections are automatically limited based on screen size: 30% (mobile), 40% (tablet), 80% (desktop). This prevents performance issues from excessive pinning on small screens.

The Beauty of Invisible Complexity

Auto-expand columns is a perfect example of why building a production-ready data grid is so challenging. What seems like "just scale everything proportionally" actually requires:

  • Sophisticated algorithms for proportional distribution with minimum width constraints
  • Section isolation for pinned columns with independent width management
  • Smart directional logic that feels natural for every column position
  • Hierarchical handling for nested headers with recursive scaling
  • Responsive constraints that prevent layout breakage on mobile
  • Performance optimization to keep interactions smooth

The best UX is invisible. Users don't think about auto-expand columns—they just expect tables to fill the screen and resize naturally. With Simple Table, you add one prop (autoExpandColumns=) and all this complexity runs invisibly in the background.

That's the power of a well-engineered data grid: sophisticated algorithms that deliver an Excel-like experience where everything just works.

Ready for tables that just work?

Simple Table's auto-expand columns deliver sophisticated proportional scaling with one prop. No complex configuration, no edge cases to handle—just tables that fill the screen beautifully.