From 27e38f3413df6a13278d6cb0a7c2c4009565f797 Mon Sep 17 00:00:00 2001 From: govindarajug <99521156+govindarajug@users.noreply.github.com> Date: Mon, 20 Apr 2026 07:56:46 +0530 Subject: [PATCH] feat: add full-width column resizing example Adds a new React example demonstrating how to make a table fill its container width with the last column stretching to occupy remaining space. Columns remain individually resizable while maintaining the full-width behavior. Uses ResizeObserver for responsive container tracking and CSS variables for performant resize rendering. This addresses a common use case discussed in #4825, #4880, #5120, and #5870 where users need tables that fill their container width with proper resize behavior. --- .../column-resizing-full-width/.gitignore | 5 + .../column-resizing-full-width/README.md | 13 ++ .../column-resizing-full-width/index.html | 13 ++ .../column-resizing-full-width/package.json | 27 +++ .../column-resizing-full-width/src/index.css | 78 +++++++ .../column-resizing-full-width/src/main.tsx | 211 ++++++++++++++++++ .../src/makeData.ts | 37 +++ .../column-resizing-full-width/tsconfig.json | 24 ++ .../column-resizing-full-width/vite.config.js | 17 ++ 9 files changed, 425 insertions(+) create mode 100644 examples/react/column-resizing-full-width/.gitignore create mode 100644 examples/react/column-resizing-full-width/README.md create mode 100644 examples/react/column-resizing-full-width/index.html create mode 100644 examples/react/column-resizing-full-width/package.json create mode 100644 examples/react/column-resizing-full-width/src/index.css create mode 100644 examples/react/column-resizing-full-width/src/main.tsx create mode 100644 examples/react/column-resizing-full-width/src/makeData.ts create mode 100644 examples/react/column-resizing-full-width/tsconfig.json create mode 100644 examples/react/column-resizing-full-width/vite.config.js diff --git a/examples/react/column-resizing-full-width/.gitignore b/examples/react/column-resizing-full-width/.gitignore new file mode 100644 index 0000000000..d451ff16c1 --- /dev/null +++ b/examples/react/column-resizing-full-width/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local diff --git a/examples/react/column-resizing-full-width/README.md b/examples/react/column-resizing-full-width/README.md new file mode 100644 index 0000000000..abea8cb79d --- /dev/null +++ b/examples/react/column-resizing-full-width/README.md @@ -0,0 +1,13 @@ +# Full-Width Column Resizing Example + +This example demonstrates how to make a TanStack Table fill its container width, with the last column automatically stretching to fill remaining space. Columns can still be individually resized while maintaining the full-width behavior. + +This pattern is useful when you want the table to always fill its container (like a spreadsheet) rather than having a fixed width based on column sizes. + +## Key Features + +- Table always fills container width +- Last column stretches to fill remaining space +- Individual columns remain resizable +- Responsive to container size changes via ResizeObserver +- Double-click a column border to reset that column's size diff --git a/examples/react/column-resizing-full-width/index.html b/examples/react/column-resizing-full-width/index.html new file mode 100644 index 0000000000..3fc40c9367 --- /dev/null +++ b/examples/react/column-resizing-full-width/index.html @@ -0,0 +1,13 @@ + + + + + + Vite App + + + +
+ + + diff --git a/examples/react/column-resizing-full-width/package.json b/examples/react/column-resizing-full-width/package.json new file mode 100644 index 0000000000..3d81f432ad --- /dev/null +++ b/examples/react/column-resizing-full-width/package.json @@ -0,0 +1,27 @@ +{ + "name": "tanstack-table-example-column-resizing-full-width", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "start": "vite", + "lint": "eslint ./src", + "test:types": "tsc" + }, + "dependencies": { + "@faker-js/faker": "^10.4.0", + "@tanstack/react-table": "^9.0.0-alpha.33", + "react": "^19.2.5", + "react-dom": "^19.2.5" + }, + "devDependencies": { + "@rollup/plugin-replace": "^6.0.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "typescript": "6.0.3", + "vite": "^8.0.8" + } +} diff --git a/examples/react/column-resizing-full-width/src/index.css b/examples/react/column-resizing-full-width/src/index.css new file mode 100644 index 0000000000..7be08266d7 --- /dev/null +++ b/examples/react/column-resizing-full-width/src/index.css @@ -0,0 +1,78 @@ +* { + box-sizing: border-box; +} + +html { + font-family: sans-serif; + font-size: 14px; +} + +.table-container { + overflow-x: auto; + border: 1px solid lightgray; +} + +.divTable { + min-width: 100%; +} + +.tr { + display: flex; +} + +tr, +.tr { + min-width: 100%; +} + +th, +.th, +td, +.td { + box-shadow: inset 0 0 0 1px lightgray; + padding: 0.25rem; +} + +th, +.th { + padding: 2px 4px; + position: relative; + font-weight: bold; + text-align: center; + height: 30px; +} + +td, +.td { + height: 30px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.resizer { + position: absolute; + top: 0; + height: 100%; + right: 0; + width: 5px; + background: rgba(0, 0, 0, 0.5); + cursor: col-resize; + user-select: none; + touch-action: none; +} + +.resizer.isResizing { + background: blue; + opacity: 1; +} + +@media (hover: hover) { + .resizer { + opacity: 0; + } + + *:hover > .resizer { + opacity: 1; + } +} diff --git a/examples/react/column-resizing-full-width/src/main.tsx b/examples/react/column-resizing-full-width/src/main.tsx new file mode 100644 index 0000000000..3702044fb7 --- /dev/null +++ b/examples/react/column-resizing-full-width/src/main.tsx @@ -0,0 +1,211 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { + columnResizingFeature, + columnSizingFeature, + createColumnHelper, + tableFeatures, + useTable, +} from '@tanstack/react-table' +import { makeData } from './makeData' +import type { Person } from './makeData' +import './index.css' + +const _features = tableFeatures({ columnSizingFeature, columnResizingFeature }) + +const columnHelper = createColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { + cell: (info) => info.getValue(), + header: 'First Name', + size: 150, + }), + columnHelper.accessor('lastName', { + cell: (info) => info.getValue(), + header: 'Last Name', + size: 150, + }), + columnHelper.accessor('age', { + header: 'Age', + size: 80, + }), + columnHelper.accessor('visits', { + header: 'Visits', + size: 100, + }), + columnHelper.accessor('status', { + header: 'Status', + size: 150, + }), + columnHelper.accessor('progress', { + header: 'Profile Progress', + size: 180, + }), +]) + +function App() { + const [data] = React.useState(() => makeData(20)) + const tableContainerRef = React.useRef(null) + const [containerWidth, setContainerWidth] = React.useState(0) + + // Track container width with ResizeObserver + React.useEffect(() => { + const container = tableContainerRef.current + if (!container) return + + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + setContainerWidth(entry.contentRect.width) + } + }) + observer.observe(container) + return () => observer.disconnect() + }, []) + + const table = useTable( + { + _features, + _rowModels: {}, + columns, + data, + defaultColumn: { + minSize: 50, + maxSize: 800, + }, + columnResizeMode: 'onChange', + }, + (state) => ({ + columnSizing: state.columnSizing, + columnResizing: state.columnResizing, + }), + ) + + const visibleColumns = table.getVisibleLeafColumns() + const totalColumnsWidth = table.getTotalSize() + + // Determine if the last column should stretch to fill remaining space + const shouldExtendLastColumn = totalColumnsWidth < containerWidth + + // Compute the width for each column, stretching the last one if needed + const getColumnWidth = React.useCallback( + (columnId: string, index: number, baseWidth: number) => { + if (shouldExtendLastColumn && index === visibleColumns.length - 1) { + const otherColumnsWidth = visibleColumns + .slice(0, -1) + .reduce((sum, col) => sum + col.getSize(), 0) + return Math.max(baseWidth, containerWidth - otherColumnsWidth) + } + return baseWidth + }, + [shouldExtendLastColumn, visibleColumns, containerWidth], + ) + + // Pre-compute column sizes as CSS variables for performant resizing + const columnSizeVars = React.useMemo(() => { + const headers = table.getFlatHeaders() + const colSizes: Record = {} + for (let i = 0; i < headers.length; i++) { + const header = headers[i] + const width = getColumnWidth( + header.column.id, + i, + header.column.getSize(), + ) + colSizes[`--header-${header.id}-size`] = width + colSizes[`--col-${header.column.id}-size`] = width + } + return colSizes + }, [table.state.columnResizing, table.state.columnSizing, getColumnWidth]) + + // Table width: always at least the container width + const tableWidth = Math.max(totalColumnsWidth, containerWidth) + + return ( +
+

Full-Width Column Resizing

+

+ The table fills its container. The last column stretches to fill any + remaining space. Try resizing individual columns — the last column + adjusts automatically. Resize the browser window to see the table adapt. +

+
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => ( +
+ {headerGroup.headers.map((header) => ( +
+ {header.isPlaceholder ? null : ( + + )} +
header.column.resetSize()} + onMouseDown={header.getResizeHandler()} + onTouchStart={header.getResizeHandler()} + className={`resizer ${ + header.column.getIsResizing() ? 'isResizing' : '' + }`} + /> +
+ ))} +
+ ))} +
+
+ {table.getRowModel().rows.map((row) => ( +
+ {row.getAllCells().map((cell) => ( +
+ {cell.renderValue()} +
+ ))} +
+ ))} +
+
+
+
+
+        {JSON.stringify(
+          {
+            containerWidth,
+            totalColumnsWidth,
+            shouldExtendLastColumn,
+            columnSizing: table.state.columnSizing,
+          },
+          null,
+          2,
+        )}
+      
+
+ ) +} + +const rootElement = document.getElementById('root') +if (!rootElement) throw new Error('Failed to find the root element') + +ReactDOM.createRoot(rootElement).render( + + + , +) diff --git a/examples/react/column-resizing-full-width/src/makeData.ts b/examples/react/column-resizing-full-width/src/makeData.ts new file mode 100644 index 0000000000..37144ed067 --- /dev/null +++ b/examples/react/column-resizing-full-width/src/makeData.ts @@ -0,0 +1,37 @@ +import { faker } from '@faker-js/faker' + +export type Person = { + firstName: string + lastName: string + age: number + visits: number + progress: number + status: 'relationship' | 'complicated' | 'single' +} + +const range = (len: number) => { + const arr: Array = [] + for (let i = 0; i < len; i++) { + arr.push(i) + } + return arr +} + +const newPerson = (): Person => { + return { + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + age: faker.number.int(40), + visits: faker.number.int(1000), + progress: faker.number.int(100), + status: faker.helpers.shuffle([ + 'relationship', + 'complicated', + 'single', + ])[0], + } +} + +export function makeData(len: number): Array { + return range(len).map(() => newPerson()) +} diff --git a/examples/react/column-resizing-full-width/tsconfig.json b/examples/react/column-resizing-full-width/tsconfig.json new file mode 100644 index 0000000000..6d545f543f --- /dev/null +++ b/examples/react/column-resizing-full-width/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/react/column-resizing-full-width/vite.config.js b/examples/react/column-resizing-full-width/vite.config.js new file mode 100644 index 0000000000..2e1361723a --- /dev/null +++ b/examples/react/column-resizing-full-width/vite.config.js @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import rollupReplace from '@rollup/plugin-replace' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + rollupReplace({ + preventAssignment: true, + values: { + __DEV__: JSON.stringify(true), + 'process.env.NODE_ENV': JSON.stringify('development'), + }, + }), + react(), + ], +})