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.
1// Example: User grows "Name" column by 100px2// Container width: 1000px (must stay constant)3// Other columns: "Email" (300px), "Role" (200px), "Status" (100px)45// Naive approach: Shrink all equally by 33.3px each6// ❌ Problem: Status (100px) shrinks by 33%, but Email (300px) only shrinks by 11%7// This feels wrong—larger columns should absorb more change89// Proportional approach: Shrink based on current width10// Total width of other columns: 600px11// Email shrinks: 100px * (300/600) = 50px → new width: 250px12// Role shrinks: 100px * (200/600) = 33.3px → new width: 166.7px13// Status shrinks: 100px * (100/600) = 16.7px → new width: 83.3px14// ✅ 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:
- Stop shrinking that column
- Redistribute its "share" of the shrinkage to other columns
- Repeat iteratively until all columns are either at minimum or properly sized
1// Iterative algorithm for minimum width constraints2function distributeWidthChange(columns, totalChange) {3 let remainingChange = totalChange;4 let activeColumns = [...columns];56 while (remainingChange > 0 && activeColumns.length > 0) {7 // Calculate proportional share for each active column8 const totalActiveWidth = activeColumns.reduce((sum, col) => sum + col.width, 0);910 for (const column of activeColumns) {11 const proportionalChange = remainingChange * (column.width / totalActiveWidth);12 const newWidth = column.width - proportionalChange;1314 if (newWidth < column.minWidth) {15 // Column hit minimum—lock it and redistribute its share16 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:
1// Initial scaling algorithm2function scaleColumnsToFit(columns, containerWidth) {3 // Sum all column widths4 const totalWidth = columns.reduce((sum, col) => sum + col.width, 0);56 // Calculate scale factor7 const scaleFactor = containerWidth / totalWidth;89 // Apply scale to all columns proportionally10 return columns.map(col => ({11 ...col,12 width: col.width * scaleFactor13 }));14}1516// Example:17// Container: 1000px18// Columns: [200px, 300px, 150px, 250px] = 900px total19// Scale factor: 1000 / 900 = 1.11120// 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:
1// Capture widths at drag start2function onResizeStart(columnId) {3 // Store initial widths for all columns4 const initialWidths = columns.map(col => ({5 id: col.id,6 width: col.currentWidth // Current rendered width7 }));89 // Use these throughout the resize operation10 // This prevents: 100 → 99.7 → 99.4 → 99.1 (rounding drift)11 // Instead: Always calculate from original 10012}
Step 3: Proportional Compensation Algorithm
As the user drags, the system calculates which columns should compensate and by how much:
1function compensateColumns(resizedColumn, widthDelta, allColumns) {2 // 1. Determine direction (left or right)3 const direction = getCompensationDirection(resizedColumn);45 // 2. Get columns that should compensate6 const columnsToCompensate = getColumnsInDirection(resizedColumn, direction);78 // 3. Calculate total width of compensating columns9 const totalCompensateWidth = columnsToCompensate.reduce(10 (sum, col) => sum + col.initialWidth,11 012 );1314 // 4. Distribute change proportionally15 const newWidths = {};16 let remainingDelta = widthDelta;1718 for (const column of columnsToCompensate) {19 // Proportional share based on initial width20 const proportion = column.initialWidth / totalCompensateWidth;21 const change = widthDelta * proportion;22 const newWidth = column.initialWidth - change; // Subtract (compensate)2324 // Check minimum width constraint25 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 }3334 // 5. If columns hit minimum, redistribute remaining delta35 if (remainingDelta > 0) {36 redistributeRemaining(newWidths, remainingDelta, columnsToCompensate);37 }3839 return newWidths;40}
Step 4: Smart Directional Logic
1function getCompensationDirection(column) {2 // Leftmost column in section → compensate right3 if (column.isLeftmost) {4 return 'right';5 }67 // Rightmost column in section → compensate left8 if (column.isRightmost) {9 return 'left';10 }1112 // Right-pinned columns → compensate left (they grow leftward)13 if (column.section === 'right-pinned') {14 return 'left';15 }1617 // Middle columns in main/left-pinned → compensate right18 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:
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 }78 // Normal columns: positive mouse delta = grow9 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
1import { SimpleTable, HeaderObject } from "simple-table-core";2import "simple-table-core/styles.css";34const 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];3637export default function AutoExpandTable({ data }) {38 return (39 <SimpleTable40 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
widthvalues 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.
1const [isMobile, setIsMobile] = useState(false);23useEffect(() => {4 const checkMobile = () => {5 setIsMobile(window.innerWidth < 768);6 };7 checkMobile();8 window.addEventListener("resize", checkMobile);9 return () => window.removeEventListener("resize", checkMobile);10}, []);1112<SimpleTable13 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:
1const headers: HeaderObject[] = [2 {3 accessor: "id",4 label: "ID",5 width: 60,6 pinned: "left", // Left-pinned section7 },8 {9 accessor: "name",10 label: "Name",11 width: 180,12 pinned: "left", // Left-pinned section13 },14 {15 accessor: "email",16 label: "Email",17 width: 250, // Main section18 },19 {20 accessor: "department",21 label: "Department",22 width: 180, // Main section23 },24 {25 accessor: "salary",26 label: "Salary",27 width: 140, // Main section28 },29 {30 accessor: "actions",31 label: "Actions",32 width: 120,33 pinned: "right", // Right-pinned section34 },35];3637<SimpleTable38 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:
1<SimpleTable2 defaultHeaders={headers}3 rows={data}4 rowIdAccessor="id"5 autoExpandColumns={true}6 isColumnResizable={true} // Enable resizing7 height="500px"8/>910// Users can now:11// 1. Drag column borders to resize12// 2. Other columns automatically compensate13// 3. Table always fills 100% of container width14// 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"
| Approach | Behavior | Best For |
|---|---|---|
autoExpandColumns | All columns scale proportionally to fill container. User resizing adjusts other columns. | Dashboards, reports, full-width layouts |
| Fixed Width | Each 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.
1const [isMobile, setIsMobile] = useState(false);23useEffect(() => {4 const checkMobile = () => {5 setIsMobile(window.innerWidth < 768);6 };7 checkMobile();8 window.addEventListener("resize", checkMobile);9 return () => window.removeEventListener("resize", checkMobile);10}, []);1112<SimpleTable13 autoExpandColumns={!isMobile} // Disable on mobile14 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.