You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[Bug] table-grouped template renders <tr> directly inside <table> — invalid DOM, and a hydration error for anyone who uses the template as a page #5277
The table-grouped page template renders <TableRow> as a direct child of <Table>, with no <TableBody> between them. The emitted DOM is <table><tr>…, which is invalid HTML. React's DOM nesting validation flags it
on every render:
In HTML, <tr> cannot be a child of <table>. Add a <tbody>, <thead> or <tfoot>
to your code to match the DOM tree generated by the browser.
This will cause a hydration error.
<table className="astryx-base-table …" style={{minWidth:"596px"}}>
<colgroup>
<TableRow role="button" tabIndex={0} …>
> <tr role="button" tabIndex={0} className="astryx-table-row …">
It is the only one of the 31 page templates with this shape. Every other
template that renders a Table uses the data-driven data={…} API, where BaseTable supplies the <tbody> itself.
Why it matters more than a console warning
A page template is starter code — the documented use is to copy it into an app
as a page. In a Next.js App Router page that is server-rendered, and there the
warning becomes a real failure: the HTML parser applies implied-<tbody>
insertion when it parses the server's markup, so the client tree and the parsed
tree disagree and hydration mismatches. Anyone who adopts this template inherits
that, in their app, not in ours.
Even client-only, the DOM is invalid: rows created through appendChild are not
reparented, so the table ends up with <tr> children and no <tbody> at all.
Any CSS or query that targets tbody — a consumer's own, or a future one in
core — silently misses.
Root cause
Table's children mode used to wrap its children in a <tbody> for you. #2097 changed that, and its own text records the old behaviour:
Today, children is dumped into a single <tbody> — after this change,
children can include XDSTableHeader, XDSTableBody, and XDSTableFooter
for full structural control.
BaseTable now renders {children ? children : <>…</>}, so children go in
raw. That is the right API. But the template was written against the implicit
wrapper and was never migrated, and nothing caught it:
No runtime or build-time diagnostic — Table accepts any children.
Table.doc.mjs still describes the prop as "Children mode: render
TableRow/TableCell directly instead of using data-driven rendering", and its
related-components list names TableRow, TableCell, and TableHeaderCell
— not TableHeader, TableBody, or TableFooter. A reader following the
component's own documentation writes exactly what this template contains.
So the template is the visible instance, but the documentation still teaches
the shape that produces it.
Fix
Three lines in assets/templates/pages/table-grouped/page.tsx — import TableBody, and wrap the groupKeys.map(…) block in it, after the <colgroup>:
TableBody is already exported from @astryxdesign/core/Table, and it is the
same <tbody> the data-driven path renders, so nothing about the styling,
dividers, or column widths changes. Verified against 0.4.3: type-checks clean,
and the nesting error goes away.
Consider warning in dev.Table in children mode could check for a
direct TableRow child and warn, the way astryx theme build already warns
about a theme naming a font it never loads — another case where the output
is well-formed and simply does not do what the author meant.
Summary
The
table-groupedpage template renders<TableRow>as a direct child of<Table>, with no<TableBody>between them. The emitted DOM is<table><tr>…, which is invalid HTML. React's DOM nesting validation flags iton every render:
It is the only one of the 31 page templates with this shape. Every other
template that renders a
Tableuses the data-drivendata={…}API, whereBaseTablesupplies the<tbody>itself.Why it matters more than a console warning
A page template is starter code — the documented use is to copy it into an app
as a page. In a Next.js App Router page that is server-rendered, and there the
warning becomes a real failure: the HTML parser applies implied-
<tbody>insertion when it parses the server's markup, so the client tree and the parsed
tree disagree and hydration mismatches. Anyone who adopts this template inherits
that, in their app, not in ours.
Even client-only, the DOM is invalid: rows created through
appendChildare notreparented, so the table ends up with
<tr>children and no<tbody>at all.Any CSS or query that targets
tbody— a consumer's own, or a future one incore — silently misses.
Root cause
Table's children mode used to wrap its children in a<tbody>for you.#2097 changed that, and its own text records the old behaviour:
BaseTablenow renders{children ? children : <>…</>}, so children go inraw. That is the right API. But the template was written against the implicit
wrapper and was never migrated, and nothing caught it:
Tableaccepts any children.reads as an addition; there is no note that children stopped being wrapped.
Table.doc.mjsstill describes the prop as "Children mode: renderTableRow/TableCell directly instead of using data-driven rendering", and its
related-components list names
TableRow,TableCell, andTableHeaderCell— not
TableHeader,TableBody, orTableFooter. A reader following thecomponent's own documentation writes exactly what this template contains.
So the template is the visible instance, but the documentation still teaches
the shape that produces it.
Fix
Three lines in
assets/templates/pages/table-grouped/page.tsx— importTableBody, and wrap thegroupKeys.map(…)block in it, after the<colgroup>:TableRow, TableCell, + TableBody, proportional,TableBodyis already exported from@astryxdesign/core/Table, and it is thesame
<tbody>the data-driven path renders, so nothing about the styling,dividers, or column widths changes. Verified against 0.4.3: type-checks clean,
and the nesting error goes away.
Suggestions beyond the template
Table'schildrendescription and itsrelated-components list should name
TableHeader/TableBody/TableFooter, and one children-mode example should show the wrapper. As itstands the docs describe the pre-feat(Table): make BaseTable private, add children-mode structural sub-components #2097 contract.
Tablein children mode could check for adirect
TableRowchild and warn, the wayastryx theme buildalready warnsabout a theme naming a font it never loads — another case where the output
is well-formed and simply does not do what the author meant.
other consumers written before it may carry the same shape.
Versions
@astryxdesign/core@0.4.3,@astryxdesign/cli@0.4.3(template assets),React 19, Next.js 16.2.x.