Reproduction
https://stackblitz.com/edit/vitejs-vite-4yccznuj?file=src%2FApp.vue
This reproduction uses the official Vite WebContainer template and starts automatically.
Environment
antdv-next@1.4.3
@v-c/table@^1.1.8
vue@^3.5.39
- Chrome on macOS
Steps to reproduce
- Render an empty table with enough columns to require horizontal scrolling.
- Set
:scroll="{ x: 'max-content' }".
- Open the table at the initial scroll position, then scroll horizontally.
Actual behavior
The empty placeholder is centered against the full table width instead of the visible viewport. With many columns it can be completely outside the initial viewport. When it becomes visible, it moves together with the horizontally scrolled table content.
Expected behavior
The empty placeholder should match Ant Design / rc-table behavior: it remains centered in the visible table viewport and does not move when the horizontal scrollbar is scrolled.
Root cause
packages/table/src/Body/ExpandedRow.tsx creates the sticky, viewport-width wrapper only after componentWidth is measured. It currently forwards contentNode to Cell through the default slot.
Cell memoizes its rendered content. The initial slot result is cached while componentWidth === 0, so the new sticky VNode produced after the resize measurement does not reach the DOM.
CellProps already declares a reactive children prop, and computeContent already prioritizes it:
const slotChildren = props.children ?? slots.default?.()
Why the slot and prop are not equivalent here
This follows Vue's documented reactivity model: the setup context and the properties of slots are not reactive. A parent slot update schedules the child to render again, but it does not itself invalidate a computed value that only reads the slots object, as also explained by a Vue maintainer.
Without memoization, rendering contentNode through the default slot or through the children prop produces the same visible child. The difference appears because Cell stores computeContent() in a Vue computed.
With the slot form, ExpandedRow evaluates contentNode in its own render and passes a slot function that closes over that value. Vue updates the slot function when the parent re-renders, but the slots object/function is not a reactive dependency of the child's computed. Calling slots.default() during the initial computation therefore caches the VNode created while componentWidth === 0; replacing the slot on a later parent update does not invalidate that computed value.
With the prop form, computeContent reads props.children. Declared Vue props are reactive inputs, so when the resize measurement makes ExpandedRow produce a new sticky VNode, the changed children prop invalidates the computed value and Cell renders the new wrapper.
Therefore this change is not relying on different rendering semantics between slots and props. It makes the changing child VNode observable to the existing memoization boundary. The prop is already declared by CellProps and already has precedence in computeContent, so this is also the smallest change that preserves the current cell memoization.
Suggested fix
Pass the reactive contentNode through the existing children prop. This is the Vue equivalent of keeping the React child current while preserving the existing memoization:
- <Cell component={cellComponent} prefixCls={prefixCls} colSpan={colSpan}>
- {contentNode}
- </Cell>
+ <Cell
+ component={cellComponent}
+ prefixCls={prefixCls}
+ colSpan={colSpan}
+ children={contentNode}
+ />
After this change, the measured wrapper is rendered with position: sticky, left: 0, and the visible component width. The placeholder stays fixed while scrolling.
Screenshots
Initial position and two horizontal scroll positions:

Reproduction
https://stackblitz.com/edit/vitejs-vite-4yccznuj?file=src%2FApp.vue
This reproduction uses the official Vite WebContainer template and starts automatically.
Environment
antdv-next@1.4.3@v-c/table@^1.1.8vue@^3.5.39Steps to reproduce
:scroll="{ x: 'max-content' }".Actual behavior
The empty placeholder is centered against the full table width instead of the visible viewport. With many columns it can be completely outside the initial viewport. When it becomes visible, it moves together with the horizontally scrolled table content.
Expected behavior
The empty placeholder should match Ant Design / rc-table behavior: it remains centered in the visible table viewport and does not move when the horizontal scrollbar is scrolled.
Root cause
packages/table/src/Body/ExpandedRow.tsxcreates the sticky, viewport-width wrapper only aftercomponentWidthis measured. It currently forwardscontentNodetoCellthrough the default slot.Cellmemoizes its rendered content. The initial slot result is cached whilecomponentWidth === 0, so the new sticky VNode produced after the resize measurement does not reach the DOM.CellPropsalready declares a reactivechildrenprop, andcomputeContentalready prioritizes it:Why the slot and prop are not equivalent here
This follows Vue's documented reactivity model: the setup context and the properties of
slotsare not reactive. A parent slot update schedules the child to render again, but it does not itself invalidate acomputedvalue that only reads the slots object, as also explained by a Vue maintainer.Without memoization, rendering
contentNodethrough the default slot or through thechildrenprop produces the same visible child. The difference appears becauseCellstorescomputeContent()in a Vuecomputed.With the slot form,
ExpandedRowevaluatescontentNodein its own render and passes a slot function that closes over that value. Vue updates the slot function when the parent re-renders, but theslotsobject/function is not a reactive dependency of the child'scomputed. Callingslots.default()during the initial computation therefore caches the VNode created whilecomponentWidth === 0; replacing the slot on a later parent update does not invalidate that computed value.With the prop form,
computeContentreadsprops.children. Declared Vue props are reactive inputs, so when the resize measurement makesExpandedRowproduce a new sticky VNode, the changedchildrenprop invalidates the computed value andCellrenders the new wrapper.Therefore this change is not relying on different rendering semantics between slots and props. It makes the changing child VNode observable to the existing memoization boundary. The prop is already declared by
CellPropsand already has precedence incomputeContent, so this is also the smallest change that preserves the current cell memoization.Suggested fix
Pass the reactive
contentNodethrough the existingchildrenprop. This is the Vue equivalent of keeping the React child current while preserving the existing memoization:After this change, the measured wrapper is rendered with
position: sticky,left: 0, and the visible component width. The placeholder stays fixed while scrolling.Screenshots
Initial position and two horizontal scroll positions: