Work in progress
A hook making table columns resizable.
- Allows resizing table columns per dragging a drag handle.
- Resets column width per double-click on the drag handle.
- Allows resizing columns via keyboard arrow keys.
See the repository's /src/demo folder for a most basic example.
useResizableColumns needs to be provided with an array of ColumnDefinition defining the order of the columns.
const columnWidthDefinitions = [
// minimal
{id: "col1"},
// all properties
{id: "col3", minWidth: 50, defaultWidth: 200},
];
const {
colGroupWidths,
containerRef,
guideRef,
onResize,
onResizeStart,
onResizeStop,
updateWidth,
widths,
} = useResizableColumns(columnWidthDefinitions);| Returned property | Description |
|---|---|
colGroupWidths |
Used to assemble the <colgroup/> |
columnWidths |
Array of current column widths in pixels, in column order |
containerRef |
Ref to be passed to the container wrapping the <table/> |
guideRef |
Ref to be passed to the guide that is rendered along the <table/> in the table container |
onResize |
Callback to be passed to the drag library's onResize callback |
onResizeStart |
Callback to be passed to the drag library's onResizeStart callback |
onResizeStop |
Callback to be passed to the drag library's onResizeStop callback |
updateWidth |
May be used to programmatically trigger updating a column's width, e.g. by implementing a key event handler |
widths |
A record of the current widths in pixel, indexed by column ids |
At this point, the hook comes without CSS, so styles need to be set up manually.
The container constraints the space available for the table and also contains the guide shown for orientation while dragging.
<div className="container">
<div className="guide"/>
<table className="resizable-table">
...
</table>
</div>The container needs the following styles:
.container {
overflow: auto;
position: relative;
}overflow: auto ensures that if the combined width of all columns is larger than the container, horizontal scrollbars will allow scrolling. position: relative is for positioning the guide.
The table layout is fixed and defined by the colgroup definition that is set up using the colgroupWidths:
<table class="resizable-table">
{colGroupWidths && (
<colgroup>
{colGroupWidths.map((width, i) => (
<col key={i} style={{ width }} />
))}
</colgroup>
)}
...
</table>The table will always at least use the whole space offered by the container. However, the combined width of all columns may be larger than the width provided by the container. Therefore, the styles the table needs:
.resizable-table {
border-collapse: collapse;
border-spacing: 0;
inline-size: fit-content;
min-width: 100%;
table-layout: fixed;
}While the combined width of all columns is smaller than the width provided by the container, the last (right-most) column will receive width: auto per its <col/> definition. Yet, there might not be sufficient space for all columns in the container, causing overflow. Whenever there is overflow, the last column will fall back to its minimum size. This simplification eases layout distribution at the cost of the last column not being resizable itself:
<thead>
<tr>
{columnDefinitions.map((columnDefinition, i) => (
// Prevent the column from being resizable when
// `i === ´columnDefinitions.length - 1`
...
))}
</tr>
</thead>The guide is a vertical line shown while dragging to give orientation about the resized column size to the user.
Example of some minimal styles:
.guide {
background-color: lightgray;
bottom: 0;
opacity: .4;
pointer-events: none;
position: absolute;
top: 0;
width: 2px;
z-index: 2;
}background-color, opacity and width can be adjusted, pointer-events: none is recommended, the remaining ones are mandatory.
The handle is a visual separator between the columns in the column headers. It is also the interaction element that can be dragged to resize a column. The handle is a custom element passed to the handle prop of the Resizable component. The implementation may look like this (respecting a11y):
const handle = (
<div
aria-label={`Resize column: ${title}`}
aria-orientation="vertical"
aria-valuemin={columnDefinition.minWidth ?? DEFAULT_MIN_WIDTH}
aria-valuenow={width}
className="handle"
onKeyDown={handleKeyDown}
role="separator"
tabIndex={0}
/>
);handleKeyDown could issue resizing per arrow keys:
const handleKeyDown = useCallback((event: KeyboardEvent) => {
if (event.key === "ArrowLeft") {
updateWidth(columnDefinition.id, width - KEY_RESIZING_STEP);
} else if (event.key === "ArrowRight") {
updateWidth(columnDefinition.id, width + KEY_RESIZING_STEP);
}
}, [columnDefinition.id, updateWidth, width]);The handle would be positioned absolute at the right edge of the header cell. It would define :before to be the visual column separator, while the handle itself would provide a larger area as click / drag target, i.e.:
.handle {
cursor: col-resize;
height: 100%;
opacity: .4;
padding: 0 4px;
position: absolute;
right: -5px;
top: 0;
width: 2px;
z-index: 1;
&:hover {
opacity: .8;
}
&:before {
background: lightgray;
content: '';
display: block;
height: 100%;
left: -1px;
position: relative;
width: 2px;
}
}For positioning the handle relative to the header cell, the cell needs to have position: relative:
.resizable-th {
position: relative;
}The hook is agnostic about which drag library is used to handle resizing. The demo uses the Resizable component from react-resizable, but any library that exposes onResizeStart, onResize, and onResizeStop callbacks can be wired up in the same way. The component is supposed to wrap each table header cell, i.e.:
const thRef = useRef<HTMLTableCellElement>(null);
const header = (
<th className="resizable-th" ref={thRef}>
{title}
</th>
);
if (!resizable) {
return header;
}
const handle = ...;
return (
<Resizable
axis="x"
handle={handle}
maxConstraints={[1000, -1]}
minConstraints={[columnDefinition.minWidth ?? DEFAULT_MIN_WIDTH, -1]}
onResize={(event, { node }) => {
onResize(event, columnDefinition.id, thRef, node);
}}
onResizeStart={onResizeStart}
onResizeStop={(event, { node }) => {
onResizeStop(event, columnDefinition.id, thRef, node);
}}
width={width}
>
{header}
</Resizable>
);Dragging is generally taken care of by react-resizable. However, for improved UX, while dragging, the cursor should remain col-resize instead of the default cursor, and it is good to disable pointer-events to disable hover effect when hovering buttons. The useResizableColumns hook adds a .col-resize class to the <html/> element while dragging, which can be used to apply the necessary styles:
html.col-resize {
cursor: col-resize;
body {
pointer-events: none;
user-select: none;
}
}