|
| 1 | +--- |
| 2 | +title: Horizontal layout for React DataGrids |
| 3 | +description: Use Infinite Table's horizontal layout to wrap rows into repeated column sets, keep grouped and tree rows readable, and make dense dashboards easier to scan. |
| 4 | +date: 2026-07-28 |
| 5 | +author: radu |
| 6 | +tags: layout, performance, dashboards |
| 7 | +--- |
| 8 | + |
| 9 | +Most DataGrids grow in one direction: more rows mean more vertical scrolling, and more columns mean more horizontal scrolling. |
| 10 | + |
| 11 | +That is usually right for record-heavy workflows, but it is not always the best use of screen space. Some dashboards have a small number of columns and many compact rows. Some operations screens need a "wallboard" view where users scan multiple row segments at once. Some tree or grouped views become easier to compare when the next batch of rows appears beside the first one, not below it. |
| 12 | + |
| 13 | +Infinite Table has a documented feature for that shape: [horizontal layout](/docs/reference/infinite-table-props#wrapRowsHorizontally), powered by <PropLink name="wrapRowsHorizontally" />. |
| 14 | + |
| 15 | +## What horizontal layout changes |
| 16 | + |
| 17 | +When horizontal layout is disabled, the grid renders one vertical list of rows. When <PropLink name="wrapRowsHorizontally" /> is enabled, Infinite Table wraps rows into multiple side-by-side column sets. |
| 18 | + |
| 19 | +If three columns are visible and ten rows fit vertically, a 25-row dataset can render as: |
| 20 | + |
| 21 | +- column set 1: rows 1-10 |
| 22 | +- column set 2: rows 11-20 |
| 23 | +- column set 3: rows 21-25 |
| 24 | + |
| 25 | +Each column set repeats the same columns. Users still read rows top to bottom inside a set, but the next page of rows starts to the right. |
| 26 | + |
| 27 | +```tsx |
| 28 | +<DataSource<Developer> primaryKey="id" data={dataSource}> |
| 29 | + <InfiniteTable<Developer> |
| 30 | + columns={columns} |
| 31 | + columnDefaultWidth={100} |
| 32 | + wrapRowsHorizontally |
| 33 | + /> |
| 34 | +</DataSource> |
| 35 | +``` |
| 36 | + |
| 37 | +That is a different layout contract from a normal grid, so it works best when every row can be understood from a small, fixed set of columns. |
| 38 | + |
| 39 | +## Try it in the docs demo |
| 40 | + |
| 41 | +The API docs include a compact example with a button that toggles horizontal layout. Enable it and resize the preview to see the grid create repeated column sets as vertical space runs out. |
| 42 | + |
| 43 | +<Sandpack title="Horizontal layout for compact row lists" size="md" viewMode="preview"> |
| 44 | + |
| 45 | +<Description> |
| 46 | + |
| 47 | +Toggle horizontal layout to see the same `id`, `firstName`, and `age` columns repeated across wrapped row sections. Full docs: [wrapRowsHorizontally](/docs/reference/infinite-table-props#wrapRowsHorizontally). |
| 48 | + |
| 49 | +</Description> |
| 50 | + |
| 51 | +```tsx live file="$DOCS/reference/horizontal-layout-example.page.tsx" |
| 52 | + |
| 53 | +``` |
| 54 | + |
| 55 | +</Sandpack> |
| 56 | + |
| 57 | +This is useful when the grid is closer to a status board than a spreadsheet: |
| 58 | + |
| 59 | +- call-center queues with id, owner, and status |
| 60 | +- warehouse picking lists with SKU, aisle, and quantity |
| 61 | +- monitoring views with service, region, and health |
| 62 | +- approval queues with requester, amount, and age |
| 63 | +- incident triage boards with priority, service, and assignee |
| 64 | + |
| 65 | +In those cases, showing more rows at once can matter more than preserving a single vertical stream. |
| 66 | + |
| 67 | +## Label repeated column sets |
| 68 | + |
| 69 | +Because the same columns repeat, it can be useful to show the column-set index in the header. Column header render functions receive `horizontalLayoutPageIndex`, which is `null` in normal layout and a zero-based number when horizontal layout is active. |
| 70 | + |
| 71 | +```tsx |
| 72 | +const getColumnHeaderFor = (label: string) => { |
| 73 | + return ({ horizontalLayoutPageIndex }) => { |
| 74 | + return ( |
| 75 | + <> |
| 76 | + {label} |
| 77 | + {horizontalLayoutPageIndex != null |
| 78 | + ? ` (${horizontalLayoutPageIndex + 1})` |
| 79 | + : ''} |
| 80 | + </> |
| 81 | + ); |
| 82 | + }; |
| 83 | +}; |
| 84 | + |
| 85 | +const columns = { |
| 86 | + id: { |
| 87 | + field: 'id', |
| 88 | + header: getColumnHeaderFor('ID'), |
| 89 | + }, |
| 90 | + firstName: { |
| 91 | + field: 'firstName', |
| 92 | + header: getColumnHeaderFor('Name'), |
| 93 | + }, |
| 94 | +}; |
| 95 | +``` |
| 96 | + |
| 97 | +The result is subtle but helpful: the user can tell whether they are reading the first, second, or third wrapped segment without losing the familiar column labels. |
| 98 | + |
| 99 | +<Sandpack title="Show the column-set index in headers" size="md" viewMode="preview"> |
| 100 | + |
| 101 | +<Description> |
| 102 | + |
| 103 | +Headers receive `horizontalLayoutPageIndex`, so each repeated column set can render a slightly different label. |
| 104 | + |
| 105 | +</Description> |
| 106 | + |
| 107 | +```tsx live file="$DOCS/reference/horizontal-layout-with-column-set-index-in-header-example.page.tsx" |
| 108 | + |
| 109 | +``` |
| 110 | + |
| 111 | +</Sandpack> |
| 112 | + |
| 113 | +You can use the same value in cell rendering when a wrapped segment needs different styling, separators, or helper text. |
| 114 | + |
| 115 | +## Keep grouped rows readable |
| 116 | + |
| 117 | +Horizontal layout becomes more interesting when the `DataSource` is grouped. A group can start in one column set and continue into the next. If the group header only appears once, the rows in the next set lose context. |
| 118 | + |
| 119 | +That is what <PropLink name="repeatWrappedGroupRows" /> is for. When enabled with <PropLink name="wrapRowsHorizontally" />, Infinite Table repeats group rows at the top of a wrapped column set when the group started in a previous set. |
| 120 | + |
| 121 | +```tsx |
| 122 | +<DataSource<Developer> |
| 123 | + primaryKey="id" |
| 124 | + data={dataSource} |
| 125 | + defaultGroupBy={[{ field: 'country' }, { field: 'city' }]} |
| 126 | +> |
| 127 | + <InfiniteTable<Developer> |
| 128 | + columns={columns} |
| 129 | + wrapRowsHorizontally |
| 130 | + repeatWrappedGroupRows |
| 131 | + /> |
| 132 | +</DataSource> |
| 133 | +``` |
| 134 | + |
| 135 | +The repeated group row acts like a sticky label for the wrapped page: users know which country or city they are still scanning. |
| 136 | + |
| 137 | +<Sandpack title="Repeat wrapped group rows" size="md" viewMode="preview"> |
| 138 | + |
| 139 | +<Description> |
| 140 | + |
| 141 | +Turn on repeated group rows to keep grouped sections readable as rows wrap into the next column set. |
| 142 | + |
| 143 | +</Description> |
| 144 | + |
| 145 | +```tsx live file="$DOCS/reference/horizontal-layout-repeat-wrapped-groups-example.page.tsx" |
| 146 | + |
| 147 | +``` |
| 148 | + |
| 149 | +</Sandpack> |
| 150 | + |
| 151 | +For tree data, <PropLink name="repeatWrappedGroupRows" /> can also be a function. That lets you repeat only the parent levels that carry useful context. |
| 152 | + |
| 153 | +```tsx |
| 154 | +<TreeGrid |
| 155 | + columns={columns} |
| 156 | + wrapRowsHorizontally |
| 157 | + repeatWrappedGroupRows={(rowInfo) => { |
| 158 | + if (!rowInfo.isTreeNode) { |
| 159 | + return false; |
| 160 | + } |
| 161 | + |
| 162 | + return rowInfo.treeNesting === 0; |
| 163 | + }} |
| 164 | +/> |
| 165 | +``` |
| 166 | + |
| 167 | +In a file-browser view, for example, repeating only top-level folders can keep each wrapped section understandable without duplicating every nested parent. |
| 168 | + |
| 169 | +## Design rules for horizontal layout |
| 170 | + |
| 171 | +Horizontal layout is intentionally advanced. It is a good fit when the content is dense, rows are compact, and users benefit from seeing more of the dataset at once. |
| 172 | + |
| 173 | +Use it when: |
| 174 | + |
| 175 | +- the row height is predictable |
| 176 | +- each row needs only a few columns |
| 177 | +- users scan rows more than they compare many fields across one row |
| 178 | +- the container is wide enough for multiple column sets |
| 179 | +- grouping or tree context can be repeated clearly |
| 180 | + |
| 181 | +Avoid it when: |
| 182 | + |
| 183 | +- columns use flexible widths, because horizontal layout expects fixed column sizing |
| 184 | +- each row has many fields that users compare left to right |
| 185 | +- the primary workflow depends on a single uninterrupted row order |
| 186 | +- the repeated headers would create more visual noise than context |
| 187 | + |
| 188 | +For traditional spreadsheet-style data entry, the normal vertical layout is usually the better default. For wallboards, operations dashboards, and dense monitoring surfaces, horizontal layout can make the same data feel much more readable. |
| 189 | + |
| 190 | +## Go deeper in the docs |
| 191 | + |
| 192 | +- [wrapRowsHorizontally](/docs/reference/infinite-table-props#wrapRowsHorizontally) - enable horizontal layout and understand column sets |
| 193 | +- [repeatWrappedGroupRows](/docs/reference/infinite-table-props#repeatWrappedGroupRows) - keep grouped and tree rows readable when wrapping |
| 194 | +- [TreeGrid with horizontal layout](/docs/learn/tree-grid/overview#working-with-horizontal-layout) - use wrapping with hierarchical data |
| 195 | +- [Fixed and flexible column sizes](/docs/learn/columns/fixed-and-flexible-size) - choose sizing rules that work with your layout |
0 commit comments