diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTable.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTable.java
index cd526b4279c..21713146776 100644
--- a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTable.java
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTable.java
@@ -26,9 +26,19 @@
/**
* Component representing a <table> element.
+ *
+ * Deprecated. This component extends {@link HtmlContainer} and therefore
+ * exposes a generic {@code add(Component)} API that allows constructing
+ * structurally invalid tables. Use {@link Table} instead, which extends
+ * {@link com.vaadin.flow.component.HtmlComponent} and exposes only
+ * spec-compliant operations (see
+ * WHATWG
+ * HTML).
*
* @since 24.4
+ * @deprecated since 25.2; use {@link Table} instead.
*/
+@Deprecated
@Tag(Tag.TABLE)
public class NativeTable extends HtmlContainer
implements ClickNotifier {
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableBody.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableBody.java
index fedbee72c76..eb92fd3d368 100644
--- a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableBody.java
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableBody.java
@@ -24,7 +24,9 @@
* Component representing a <tbody> element.
*
* @since 24.4
+ * @deprecated since 25.2; use {@link TableBody} instead.
*/
+@Deprecated
@Tag(Tag.TBODY)
public class NativeTableBody extends HtmlContainer
implements NativeTableRowContainer, ClickNotifier {
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableCaption.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableCaption.java
index 628056f39f0..81a4d0eee33 100644
--- a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableCaption.java
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableCaption.java
@@ -22,7 +22,9 @@
* Represents the table caption element ({@code }).
*
* @since 24.4
+ * @deprecated since 25.2; use {@link TableCaption} instead.
*/
+@Deprecated
@Tag(Tag.CAPTION)
public class NativeTableCaption extends HtmlContainer {
}
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableCell.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableCell.java
index 16eb226d858..eb24b4e2b14 100644
--- a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableCell.java
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableCell.java
@@ -27,7 +27,9 @@
* Component representing a <td> element.
*
* @since 24.4
+ * @deprecated since 25.2; use {@link TableDataCell} instead.
*/
+@Deprecated
@Tag(Tag.TD)
public class NativeTableCell extends HtmlContainer
implements ClickNotifier {
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableFooter.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableFooter.java
index 3ddd68c984f..2334a7066a2 100644
--- a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableFooter.java
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableFooter.java
@@ -24,7 +24,9 @@
* Component representing a <tfoot> element.
*
* @since 24.4
+ * @deprecated since 25.2; use {@link TableFoot} instead.
*/
+@Deprecated
@Tag(Tag.TFOOT)
public class NativeTableFooter extends HtmlContainer
implements NativeTableRowContainer, ClickNotifier {
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableHeader.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableHeader.java
index ad797a64e7b..892b92dd32a 100644
--- a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableHeader.java
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableHeader.java
@@ -24,7 +24,9 @@
* Component representing a <thead> element.
*
* @since 24.4
+ * @deprecated since 25.2; use {@link TableHead} instead.
*/
+@Deprecated
@Tag(Tag.THEAD)
public class NativeTableHeader extends HtmlContainer
implements NativeTableRowContainer, ClickNotifier {
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableHeaderCell.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableHeaderCell.java
index 81b4d8b03e4..5117651cdd0 100644
--- a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableHeaderCell.java
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableHeaderCell.java
@@ -27,7 +27,9 @@
* Component representing a <th> element.
*
* @since 24.4
+ * @deprecated since 25.2; use {@link TableHeaderCell} instead.
*/
+@Deprecated
@Tag(Tag.TH)
public class NativeTableHeaderCell extends HtmlContainer
implements ClickNotifier {
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableRow.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableRow.java
index 2d022697a76..e3d67036dcf 100644
--- a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableRow.java
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableRow.java
@@ -29,7 +29,9 @@
* Component representing a <tr> element.
*
* @since 24.4
+ * @deprecated since 25.2; use {@link TableRow} instead.
*/
+@Deprecated
@Tag(Tag.TR)
public class NativeTableRow extends HtmlContainer
implements HasOrderedComponents, ClickNotifier {
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableRowContainer.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableRowContainer.java
index 0fd3159f5f3..fcd1ad5824c 100644
--- a/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableRowContainer.java
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/NativeTableRowContainer.java
@@ -26,7 +26,11 @@
* A container of <tr> elements.
*
* @since 24.4
+ * @deprecated since 25.2; use {@link TableRowContainer} (and the corresponding
+ * {@link TableHead}, {@link TableBody}, and {@link TableFoot}
+ * components) instead.
*/
+@Deprecated
interface NativeTableRowContainer extends HasOrderedComponents {
/**
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/Table.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/Table.java
new file mode 100644
index 00000000000..b563b03af75
--- /dev/null
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/Table.java
@@ -0,0 +1,576 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+
+import com.vaadin.flow.component.ClickNotifier;
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.HtmlComponent;
+import com.vaadin.flow.component.Tag;
+
+/**
+ * Component representing a <table> element — a
+ * two-dimensional grid of cells with optional header, body and footer sections,
+ * captioning and column-level styling.
+ *
+ * Per the WHATWG
+ * HTML specification, a <table> may contain (in order):
+ * an optional <caption>, zero or more
+ * <colgroup> elements, an optional
+ * <thead>, zero or more <tbody> elements,
+ * and an optional <tfoot>. This component therefore extends
+ * {@link HtmlComponent} (rather than
+ * {@link com.vaadin.flow.component.HtmlContainer}) and exposes only the
+ * structured operations required to build a valid table — child components are
+ * inserted at the correct position automatically.
+ *
+ * @see MDN:
+ * <table> — The Table element
+ * @since 25.2
+ */
+@Tag(Tag.TABLE)
+public class Table extends HtmlComponent implements ClickNotifier
{
+
+ private TableCaption caption;
+ private final List columnGroups = new LinkedList<>();
+ private TableHead head;
+ private final List bodies = new LinkedList<>();
+ private TableFoot foot;
+
+ /**
+ * Creates a new empty table.
+ */
+ public Table() {
+ super();
+ }
+
+ /**
+ * Return the table's caption component. Creates a new instance if no
+ * caption is present.
+ *
+ * @return the table's caption.
+ */
+ public TableCaption getCaption() {
+ if (caption == null) {
+ caption = new TableCaption();
+ getElement().insertChild(0, caption.getElement());
+ }
+ return caption;
+ }
+
+ /**
+ * Returns the caption component if one has been set.
+ *
+ * @return an {@link Optional} containing the caption, or empty if none.
+ */
+ public Optional findCaption() {
+ return Optional.ofNullable(caption);
+ }
+
+ /**
+ * Returns the caption text for this table, or an empty string if no caption
+ * has been set.
+ *
+ * @return the table's caption text.
+ */
+ public String getCaptionText() {
+ return caption == null ? "" : caption.getText();
+ }
+
+ /**
+ * Sets the caption text for this table. Creates a caption element if none
+ * exists.
+ *
+ * @param text
+ * the caption's text
+ */
+ public void setCaptionText(String text) {
+ getCaption().setText(text);
+ }
+
+ /**
+ * Appends the given components to this table's caption, creating it if none
+ * exists yet. Useful for richer captions containing inline markup.
+ *
+ * @param components
+ * the components to append.
+ * @return the caption.
+ */
+ public TableCaption addCaption(Component... components) {
+ return addCaption(Arrays.asList(components));
+ }
+
+ /**
+ * List equivalent of {@link #addCaption(Component...)}.
+ *
+ * @param components
+ * the components to append.
+ * @return the caption.
+ */
+ public TableCaption addCaption(List extends Component> components) {
+ TableCaption c = getCaption();
+ c.add(components.toArray(Component[]::new));
+ return c;
+ }
+
+ /**
+ * Remove the caption from this table.
+ */
+ public void removeCaption() {
+ if (caption != null) {
+ getElement().removeChild(caption.getElement());
+ caption = null;
+ }
+ }
+
+ /**
+ * Appends a new empty {@code } to this table.
+ *
+ * @return the newly created column group.
+ */
+ public TableColumnGroup addColumnGroup() {
+ TableColumnGroup group = new TableColumnGroup();
+ getElement().insertChild(columnGroupAppendIndex(), group.getElement());
+ columnGroups.add(group);
+ return group;
+ }
+
+ /**
+ * Appends an existing {@code } to this table.
+ *
+ * @param group
+ * the column group to add.
+ * @return the same group, for fluent chaining.
+ */
+ public TableColumnGroup addColumnGroup(TableColumnGroup group) {
+ getElement().insertChild(columnGroupAppendIndex(), group.getElement());
+ columnGroups.add(group);
+ return group;
+ }
+
+ /**
+ * Appends a new {@code } populated with the given columns.
+ *
+ * @param columns
+ * the columns to place inside the new group.
+ * @return the newly created column group.
+ */
+ public TableColumnGroup addColumnGroup(TableColumn... columns) {
+ return addColumnGroup(Arrays.asList(columns));
+ }
+
+ /**
+ * List equivalent of {@link #addColumnGroup(TableColumn...)}.
+ *
+ * @param columns
+ * the columns to place inside the new group.
+ * @return the newly created column group.
+ */
+ public TableColumnGroup addColumnGroup(
+ List extends TableColumn> columns) {
+ TableColumnGroup group = new TableColumnGroup();
+ getElement().insertChild(columnGroupAppendIndex(), group.getElement());
+ columnGroups.add(group);
+ group.addColumns(columns);
+ return group;
+ }
+
+ /**
+ * Returns the column groups attached to this table, in document order.
+ *
+ * @return an unmodifiable list of column groups.
+ */
+ public List getColumnGroups() {
+ return Collections.unmodifiableList(new ArrayList<>(columnGroups));
+ }
+
+ /**
+ * Removes a column group from this table.
+ *
+ * @param group
+ * the group to remove.
+ */
+ public void removeColumnGroup(TableColumnGroup group) {
+ if (columnGroups.remove(group)) {
+ getElement().removeChild(group.getElement());
+ }
+ }
+
+ /**
+ * Returns the head of this table. Creates a new one if none was present,
+ * inserted at the correct position (after the caption and any column
+ * groups).
+ *
+ * @return this table's {@code } element.
+ */
+ public TableHead getHead() {
+ if (head == null) {
+ head = new TableHead();
+ getElement().insertChild(headIndex(), head.getElement());
+ }
+ return head;
+ }
+
+ /**
+ * Returns the head if one has been set.
+ *
+ * @return an {@link Optional} containing the head, or empty if none.
+ */
+ public Optional findHead() {
+ return Optional.ofNullable(head);
+ }
+
+ /**
+ * Remove the head from this table, if present.
+ */
+ public void removeHead() {
+ if (head != null) {
+ getElement().removeChild(head.getElement());
+ head = null;
+ }
+ }
+
+ /**
+ * Returns the {@code } element of this table. Creates a new one if
+ * none was present, appended at the end of the table per the HTML spec.
+ *
+ * @return the {@code } element of this table.
+ */
+ public TableFoot getFoot() {
+ if (foot == null) {
+ foot = new TableFoot();
+ getElement().appendChild(foot.getElement());
+ }
+ return foot;
+ }
+
+ /**
+ * Returns the foot if one has been set.
+ *
+ * @return an {@link Optional} containing the foot, or empty if none.
+ */
+ public Optional findFoot() {
+ return Optional.ofNullable(foot);
+ }
+
+ /**
+ * Removes the foot from this table, if present.
+ */
+ public void removeFoot() {
+ if (foot != null) {
+ getElement().removeChild(foot.getElement());
+ foot = null;
+ }
+ }
+
+ /**
+ * Returns the list of {@code } elements in this table.
+ *
+ * @return an unmodifiable list of body elements.
+ */
+ public List getBodies() {
+ return Collections.unmodifiableList(new ArrayList<>(bodies));
+ }
+
+ /**
+ * Returns the first body element in this table. Creates one if there's
+ * none.
+ *
+ * @return the first {@code } element in the table.
+ */
+ public TableBody getBody() {
+ if (bodies.isEmpty()) {
+ return addBody();
+ }
+ return bodies.get(0);
+ }
+
+ /**
+ * Adds a new body element to the table, positioned after the existing
+ * bodies and before the foot (if any).
+ *
+ * @return the new body.
+ */
+ public TableBody addBody() {
+ TableBody body = new TableBody();
+ getElement().insertChild(bodyAppendIndex(), body.getElement());
+ bodies.add(body);
+ return body;
+ }
+
+ /**
+ * Removes a body element from the table.
+ *
+ * @param body
+ * the body component to remove.
+ */
+ public void removeBody(TableBody body) {
+ if (bodies.remove(body)) {
+ getElement().removeChild(body.getElement());
+ }
+ }
+
+ /**
+ * Returns every {@link TableRow} in this table — the head's rows, then the
+ * rows of each body in order, then the foot's rows — matching the document
+ * order exposed by the browser DOM's {@code HTMLTableElement.rows}. Useful
+ * for "iterate all rows" or "count rows" cases; for structural work go
+ * through {@link #getHead()}, {@link #getBody()} or {@link #getFoot()}
+ * directly.
+ *
+ * @return an unmodifiable list of all rows in the table.
+ */
+ public List getRows() {
+ List all = new ArrayList<>();
+ if (head != null) {
+ all.addAll(head.getRows());
+ }
+ for (TableBody body : bodies) {
+ all.addAll(body.getRows());
+ }
+ if (foot != null) {
+ all.addAll(foot.getRows());
+ }
+ return Collections.unmodifiableList(all);
+ }
+
+ /**
+ * Removes every row from this table's head, bodies and foot. The section
+ * elements themselves ({@code }, {@code }, {@code })
+ * and any column groups are kept; use {@link #removeHead()},
+ * {@link #removeBody(TableBody)} or {@link #removeFoot()} to drop those.
+ */
+ public void removeAllRows() {
+ if (head != null) {
+ head.removeAllRows();
+ }
+ for (TableBody body : bodies) {
+ body.removeAllRows();
+ }
+ if (foot != null) {
+ foot.removeAllRows();
+ }
+ }
+
+ /**
+ * Appends a new empty row to this table's body, creating an implicit
+ * {@code } if none exists yet. Mirrors the HTML pattern of placing
+ * <tr> elements directly inside a
+ * <table> (the browser auto-wraps them in
+ * {@code }).
+ *
+ * @return the newly created row.
+ */
+ public TableRow addRow() {
+ return getBody().addRow();
+ }
+
+ /**
+ * Appends a new row containing the given texts as data cells
+ * (<td>) to this table's body, creating an implicit
+ * {@code } if none exists yet.
+ *
+ * @param cellTexts
+ * the text content for each data cell.
+ * @return the newly created row.
+ */
+ public TableRow addRow(String... cellTexts) {
+ return addRow(Arrays.asList(cellTexts));
+ }
+
+ /**
+ * List equivalent of {@link #addRow(String...)}.
+ *
+ * @param cellTexts
+ * the text content for each data cell.
+ * @return the newly created row.
+ */
+ public TableRow addRow(List cellTexts) {
+ TableRow row = getBody().addRow();
+ row.addDataCells(cellTexts);
+ return row;
+ }
+
+ /**
+ * Appends the given rows to this table's body, creating an implicit
+ * {@code } if none exists yet.
+ *
+ * @param rows
+ * the rows to add.
+ */
+ public void addRows(TableRow... rows) {
+ getBody().addRows(rows);
+ }
+
+ /**
+ * List equivalent of {@link #addRows(TableRow...)}.
+ *
+ * @param rows
+ * the rows to add.
+ */
+ public void addRows(List extends TableRow> rows) {
+ getBody().addRows(rows);
+ }
+
+ /**
+ * Appends a new empty row to this table's {@code }, creating it if
+ * none exists yet.
+ *
+ * @return the newly created row.
+ */
+ public TableRow addHeaderRow() {
+ return getHead().addRow();
+ }
+
+ /**
+ * Appends a new row containing the given texts as header cells
+ * (<th>) to this table's {@code }, creating it if
+ * none exists yet.
+ *
+ * @param cellTexts
+ * the text content for each header cell.
+ * @return the newly created row.
+ */
+ public TableRow addHeaderRow(String... cellTexts) {
+ return addHeaderRow(Arrays.asList(cellTexts));
+ }
+
+ /**
+ * List equivalent of {@link #addHeaderRow(String...)}.
+ *
+ * @param cellTexts
+ * the text content for each header cell.
+ * @return the newly created row.
+ */
+ public TableRow addHeaderRow(List cellTexts) {
+ TableRow row = getHead().addRow();
+ row.addHeaderCells(cellTexts);
+ return row;
+ }
+
+ /**
+ * Appends the given rows to this table's {@code }, creating it if
+ * none exists yet.
+ *
+ * @param rows
+ * the rows to add.
+ */
+ public void addHeaderRows(TableRow... rows) {
+ getHead().addRows(rows);
+ }
+
+ /**
+ * List equivalent of {@link #addHeaderRows(TableRow...)}.
+ *
+ * @param rows
+ * the rows to add.
+ */
+ public void addHeaderRows(List extends TableRow> rows) {
+ getHead().addRows(rows);
+ }
+
+ /**
+ * Appends a new empty row to this table's {@code }, creating it if
+ * none exists yet.
+ *
+ * @return the newly created row.
+ */
+ public TableRow addFooterRow() {
+ return getFoot().addRow();
+ }
+
+ /**
+ * Appends a new row containing the given texts as data cells
+ * (<td>) to this table's {@code }, creating it if
+ * none exists yet.
+ *
+ * @param cellTexts
+ * the text content for each data cell.
+ * @return the newly created row.
+ */
+ public TableRow addFooterRow(String... cellTexts) {
+ return addFooterRow(Arrays.asList(cellTexts));
+ }
+
+ /**
+ * List equivalent of {@link #addFooterRow(String...)}.
+ *
+ * @param cellTexts
+ * the text content for each data cell.
+ * @return the newly created row.
+ */
+ public TableRow addFooterRow(List cellTexts) {
+ TableRow row = getFoot().addRow();
+ row.addDataCells(cellTexts);
+ return row;
+ }
+
+ /**
+ * Appends the given rows to this table's {@code }, creating it if
+ * none exists yet.
+ *
+ * @param rows
+ * the rows to add.
+ */
+ public void addFooterRows(TableRow... rows) {
+ getFoot().addRows(rows);
+ }
+
+ /**
+ * List equivalent of {@link #addFooterRows(TableRow...)}.
+ *
+ * @param rows
+ * the rows to add.
+ */
+ public void addFooterRows(List extends TableRow> rows) {
+ getFoot().addRows(rows);
+ }
+
+ private int columnGroupAppendIndex() {
+ int index = columnGroups.size();
+ if (caption != null) {
+ index++;
+ }
+ return index;
+ }
+
+ private int headIndex() {
+ int index = columnGroups.size();
+ if (caption != null) {
+ index++;
+ }
+ return index;
+ }
+
+ private int bodyAppendIndex() {
+ int index = bodies.size() + columnGroups.size();
+ if (caption != null) {
+ index++;
+ }
+ if (head != null) {
+ index++;
+ }
+ return index;
+ }
+
+}
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableBody.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableBody.java
new file mode 100644
index 00000000000..5cef4a95679
--- /dev/null
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableBody.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import java.util.List;
+
+import com.vaadin.flow.component.ClickNotifier;
+import com.vaadin.flow.component.HtmlComponent;
+import com.vaadin.flow.component.Tag;
+
+/**
+ * Component representing a <tbody> element.
+ *
+ * Per the WHATWG
+ * HTML specification, a {@code
} may only contain
+ * <tr> elements. This component therefore extends
+ * {@link HtmlComponent} (rather than
+ * {@link com.vaadin.flow.component.HtmlContainer}) and exposes only
+ * {@link TableRow}-specific operations through {@link TableRowContainer}.
+ *
+ * @since 25.2
+ */
+@Tag(Tag.TBODY)
+public class TableBody extends HtmlComponent
+ implements TableRowContainer, ClickNotifier {
+
+ /**
+ * Creates a new empty table body.
+ */
+ public TableBody() {
+ super();
+ }
+
+ /**
+ * Creates a new table body with the given rows.
+ *
+ * @param rows
+ * the rows to add.
+ */
+ public TableBody(TableRow... rows) {
+ super();
+ addRows(rows);
+ }
+
+ /**
+ * List equivalent of {@link #TableBody(TableRow...)}.
+ *
+ * @param rows
+ * the rows to add.
+ */
+ public TableBody(List extends TableRow> rows) {
+ super();
+ addRows(rows);
+ }
+}
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableCaption.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableCaption.java
new file mode 100644
index 00000000000..185d28e2001
--- /dev/null
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableCaption.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import com.vaadin.flow.component.HtmlContainer;
+import com.vaadin.flow.component.Tag;
+
+/**
+ * Represents the table caption element ({@code }).
+ *
+ * @since 25.2
+ */
+@Tag(Tag.CAPTION)
+public class TableCaption extends HtmlContainer {
+}
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableCell.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableCell.java
new file mode 100644
index 00000000000..f0b02d8c2c5
--- /dev/null
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableCell.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.HtmlContainer;
+
+/**
+ * Common superclass for table cell components ({@link TableDataCell} and
+ * {@link TableHeaderCell}). Provides shared support for the attributes that
+ * apply equally to <td> and <th> per the
+ * WHATWG HTML
+ * specification: {@code colspan}, {@code rowspan} and {@code headers}.
+ *
+ * @see MDN:
+ * <td>
+ * @see MDN:
+ * <th>
+ * @since 25.2
+ */
+public abstract class TableCell extends HtmlContainer {
+
+ private static final String ATTRIBUTE_COLSPAN = "colspan";
+ private static final String ATTRIBUTE_ROWSPAN = "rowspan";
+ private static final String ATTRIBUTE_HEADERS = "headers";
+
+ /**
+ * Creates a new empty cell component.
+ */
+ protected TableCell() {
+ super();
+ }
+
+ /**
+ * Creates a new cell with the given children components.
+ *
+ * @param components
+ * the children components.
+ */
+ protected TableCell(Component... components) {
+ super(components);
+ }
+
+ /**
+ * List equivalent of {@link #TableCell(Component...)}.
+ *
+ * @param components
+ * the children components.
+ */
+ protected TableCell(List extends Component> components) {
+ super(components.toArray(Component[]::new));
+ }
+
+ /**
+ * Sets the {@code colspan} attribute — how many columns this cell spans.
+ * The default is {@code 1}. Browsers clamp values higher than 1000 back to
+ * {@code 1}.
+ *
+ * @param colspan
+ * a non-negative integer.
+ */
+ public void setColspan(int colspan) {
+ if (colspan < 0) {
+ throw new IllegalArgumentException(
+ "colspan must be a non-negative integer value");
+ }
+ getElement().setAttribute(ATTRIBUTE_COLSPAN, String.valueOf(colspan));
+ }
+
+ /**
+ * Returns the colspan value of this cell.
+ *
+ * @return the current value of the colspan attribute. Default is 1.
+ */
+ public int getColspan() {
+ String colspan = getElement().getAttribute(ATTRIBUTE_COLSPAN);
+ if (colspan == null) {
+ colspan = "1";
+ }
+ return Integer.parseInt(colspan);
+ }
+
+ /**
+ * Reset colspan to its default value of 1.
+ */
+ public void resetColspan() {
+ getElement().removeAttribute(ATTRIBUTE_COLSPAN);
+ }
+
+ /**
+ * Sets the {@code rowspan} attribute — how many rows this cell spans. The
+ * default is {@code 1}. A value of {@code 0} extends the cell until the end
+ * of its grouping section ({@code }, {@code } or
+ * {@code }, even if implicitly defined). Browsers clip values above
+ * 65534.
+ *
+ * @param rowspan
+ * a non-negative integer.
+ */
+ public void setRowspan(int rowspan) {
+ if (rowspan < 0) {
+ throw new IllegalArgumentException(
+ "rowspan must be a non-negative integer value");
+ }
+ getElement().setAttribute(ATTRIBUTE_ROWSPAN, String.valueOf(rowspan));
+ }
+
+ /**
+ * Returns the rowspan value of this cell.
+ *
+ * @return the current value of the rowspan attribute. Default is 1.
+ */
+ public int getRowspan() {
+ String rowspan = getElement().getAttribute(ATTRIBUTE_ROWSPAN);
+ if (rowspan == null) {
+ rowspan = "1";
+ }
+ return Integer.parseInt(rowspan);
+ }
+
+ /**
+ * Resets the rowspan to its default value of 1.
+ */
+ public void resetRowspan() {
+ getElement().removeAttribute(ATTRIBUTE_ROWSPAN);
+ }
+
+ /**
+ * Sets the {@code headers} attribute — a list of ids referring to the
+ * <th> cells that label this cell. Assistive
+ * technologies use it to read out the right headers when navigating complex
+ * tables, where {@link TableHeaderCell#setScope(TableHeaderCell.Scope)
+ * scope} alone isn't enough to disambiguate.
+ *
+ * Passing no arguments (or an empty array) removes the attribute.
+ *
+ * @param ids
+ * the ids of the header cells, in any order.
+ */
+ public void setHeaders(String... ids) {
+ setHeaders(ids == null ? List.of() : Arrays.asList(ids));
+ }
+
+ /**
+ * List equivalent of {@link #setHeaders(String...)}. An empty list (or
+ * {@code null}) clears the attribute.
+ *
+ * @param ids
+ * the ids of the header cells, in any order.
+ */
+ public void setHeaders(List ids) {
+ if (ids == null || ids.isEmpty()) {
+ getElement().removeAttribute(ATTRIBUTE_HEADERS);
+ return;
+ }
+ for (String id : ids) {
+ Objects.requireNonNull(id, "header id must not be null");
+ }
+ getElement().setAttribute(ATTRIBUTE_HEADERS, String.join(" ", ids));
+ }
+
+ /**
+ * Convenience overload that takes header cells directly and uses their
+ * {@code id} attributes. Each cell must have an id set.
+ *
+ * @param headerCells
+ * the header cells whose ids should be referenced.
+ * @throws IllegalArgumentException
+ * if any of the given cells does not have an id set.
+ */
+ public void setHeaders(TableHeaderCell... headerCells) {
+ setHeadersByCells(
+ headerCells == null ? List.of() : Arrays.asList(headerCells));
+ }
+
+ /**
+ * List equivalent of {@link #setHeaders(TableHeaderCell...)}.
+ *
+ * @param headerCells
+ * the header cells whose ids should be referenced.
+ * @throws IllegalArgumentException
+ * if any of the given cells does not have an id set.
+ */
+ public void setHeadersByCells(List extends TableHeaderCell> headerCells) {
+ if (headerCells == null || headerCells.isEmpty()) {
+ getElement().removeAttribute(ATTRIBUTE_HEADERS);
+ return;
+ }
+ List ids = new ArrayList<>(headerCells.size());
+ for (TableHeaderCell cell : headerCells) {
+ ids.add(cell.getId().orElseThrow(() -> new IllegalArgumentException(
+ "Header cell must have an id to be referenced via the headers attribute")));
+ }
+ setHeaders(ids);
+ }
+
+ /**
+ * Returns the IDs of the header cells associated with this cell via the
+ * {@code headers} attribute, or an empty {@link Optional} if the attribute
+ * is not set.
+ *
+ * @return the parsed list of header IDs.
+ */
+ public Optional getHeaders() {
+ String value = getElement().getAttribute(ATTRIBUTE_HEADERS);
+ if (value == null || value.isEmpty()) {
+ return Optional.empty();
+ }
+ return Optional.of(value.split("\\s+"));
+ }
+
+ /**
+ * Removes the {@code headers} attribute from this cell.
+ */
+ public void resetHeaders() {
+ getElement().removeAttribute(ATTRIBUTE_HEADERS);
+ }
+}
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableColumn.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableColumn.java
new file mode 100644
index 00000000000..58bd5e77f51
--- /dev/null
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableColumn.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import com.vaadin.flow.component.HtmlComponent;
+import com.vaadin.flow.component.Tag;
+
+/**
+ * Component representing a <col> element — a column (or
+ * range of columns, via {@link #setSpan(int)}) inside a
+ * {@link TableColumnGroup}. Use it to apply column-wide styling without
+ * repeating it on every cell: a class or id on a {@code } can target all
+ * the data cells in that column.
+ *
+ * {@code
} is a void element (no children, no end tag) and is only valid
+ * inside a {@code } that does not itself carry a {@code span}
+ * attribute. Only a limited subset of CSS applies to columns:
+ * {@code background}, {@code border} (with {@code border-collapse: collapse}),
+ * {@code visibility: collapse} and {@code width}. Text and font properties do
+ * not inherit into the cells — style those on <td> or
+ * <th> instead.
+ *
+ * @see MDN:
+ * <col> — The Table Column element
+ * @since 25.2
+ */
+@Tag(Tag.COL)
+public class TableColumn extends HtmlComponent {
+
+ private static final String ATTRIBUTE_SPAN = "span";
+
+ /**
+ * Creates a new column component spanning a single column.
+ */
+ public TableColumn() {
+ super();
+ }
+
+ /**
+ * Creates a new column component spanning the given number of columns.
+ *
+ * @param span
+ * the number of consecutive columns this {@code } element
+ * applies to. Must be a positive integer.
+ */
+ public TableColumn(int span) {
+ super();
+ setSpan(span);
+ }
+
+ /**
+ * Sets the {@code span} attribute — how many consecutive columns this
+ * {@code } element covers. The default is {@code 1}. Use it to apply
+ * the same styling or attributes across a range of columns without writing
+ * one {@code } per column.
+ *
+ * @param span
+ * a positive integer.
+ */
+ public void setSpan(int span) {
+ if (span < 1) {
+ throw new IllegalArgumentException(
+ "span must be a positive integer value");
+ }
+ getElement().setAttribute(ATTRIBUTE_SPAN, String.valueOf(span));
+ }
+
+ /**
+ * Returns the value of the {@code span} attribute.
+ *
+ * @return the current span. Default is 1.
+ */
+ public int getSpan() {
+ String span = getElement().getAttribute(ATTRIBUTE_SPAN);
+ if (span == null) {
+ span = "1";
+ }
+ return Integer.parseInt(span);
+ }
+
+ /**
+ * Resets the span to its default value of 1.
+ */
+ public void resetSpan() {
+ getElement().removeAttribute(ATTRIBUTE_SPAN);
+ }
+}
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableColumnGroup.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableColumnGroup.java
new file mode 100644
index 00000000000..3bbfbb5539b
--- /dev/null
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableColumnGroup.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.vaadin.flow.component.HtmlComponent;
+import com.vaadin.flow.component.Tag;
+
+/**
+ * Component representing a <colgroup> element — a group of
+ * columns inside a {@link Table}, used to apply attributes (most often a class
+ * for CSS) to several columns at once. Only a limited subset of CSS applies to
+ * column groups: {@code background}, {@code border} (with
+ * {@code border-collapse: collapse}), {@code visibility: collapse} and
+ * {@code width}.
+ *
+ * Per the WHATWG
+ * HTML specification, a {@code
} is used in one of two modes:
+ * either it carries a {@code span} attribute and has no children, or it
+ * contains zero or more {@code } children and has no {@code span}
+ * attribute. This component therefore extends {@link HtmlComponent} (rather
+ * than {@link com.vaadin.flow.component.HtmlContainer}) and exposes only
+ * operations for managing {@link TableColumn} children plus the {@code span}
+ * attribute. {@code } elements must be placed after the optional
+ * {@code } and before any {@code }, {@code },
+ * {@code } or <tr>; the {@link Table} inserts them at
+ * the correct position automatically.
+ *
+ * @see MDN:
+ * <colgroup> — The Table Column Group element
+ * @since 25.2
+ */
+@Tag(Tag.COLGROUP)
+public class TableColumnGroup extends HtmlComponent {
+
+ private static final String ATTRIBUTE_SPAN = "span";
+
+ /**
+ * Creates a new empty column group.
+ */
+ public TableColumnGroup() {
+ super();
+ }
+
+ /**
+ * Creates a new column group with the given columns appended.
+ *
+ * @param columns
+ * the columns to add.
+ */
+ public TableColumnGroup(TableColumn... columns) {
+ this(Arrays.asList(columns));
+ }
+
+ /**
+ * List equivalent of {@link #TableColumnGroup(TableColumn...)}.
+ *
+ * @param columns
+ * the columns to add.
+ */
+ public TableColumnGroup(List extends TableColumn> columns) {
+ super();
+ addColumns(columns);
+ }
+
+ /**
+ * Appends a new empty {@code } child to this group.
+ *
+ * @return the new column.
+ */
+ public TableColumn addColumn() {
+ TableColumn column = new TableColumn();
+ getElement().appendChild(column.getElement());
+ return column;
+ }
+
+ /**
+ * Appends a new {@code } child with the given span to this group.
+ *
+ * @param span
+ * the number of columns to span.
+ * @return the new column.
+ */
+ public TableColumn addColumn(int span) {
+ TableColumn column = new TableColumn(span);
+ getElement().appendChild(column.getElement());
+ return column;
+ }
+
+ /**
+ * Appends the given columns to this group.
+ *
+ * @param columns
+ * the columns to add.
+ */
+ public void addColumns(TableColumn... columns) {
+ addColumns(Arrays.asList(columns));
+ }
+
+ /**
+ * List equivalent of {@link #addColumns(TableColumn...)}.
+ *
+ * @param columns
+ * the columns to add.
+ */
+ public void addColumns(List extends TableColumn> columns) {
+ for (TableColumn column : columns) {
+ getElement().appendChild(column.getElement());
+ }
+ }
+
+ /**
+ * Returns the columns inside this group.
+ *
+ * @return the list of {@code } children.
+ */
+ public List getColumns() {
+ return getChildren().filter(c -> c instanceof TableColumn)
+ .map(c -> (TableColumn) c).collect(Collectors.toList());
+ }
+
+ /**
+ * Removes a column from this group.
+ *
+ * @param column
+ * the column to remove.
+ */
+ public void removeColumn(TableColumn column) {
+ getElement().removeChild(column.getElement());
+ }
+
+ /**
+ * Removes all columns from this group.
+ */
+ public void removeAllColumns() {
+ getElement().removeAllChildren();
+ }
+
+ /**
+ * Sets the {@code span} attribute — how many consecutive columns this group
+ * covers when used without {@link TableColumn} children. Per the HTML
+ * specification, {@code span} is only valid on a {@code } that
+ * has no {@code } children. The default is {@code 1}.
+ *
+ * @param span
+ * a positive integer.
+ */
+ public void setSpan(int span) {
+ if (span < 1) {
+ throw new IllegalArgumentException(
+ "span must be a positive integer value");
+ }
+ getElement().setAttribute(ATTRIBUTE_SPAN, String.valueOf(span));
+ }
+
+ /**
+ * Returns the value of the {@code span} attribute.
+ *
+ * @return the current span. Default is 1.
+ */
+ public int getSpan() {
+ String span = getElement().getAttribute(ATTRIBUTE_SPAN);
+ if (span == null) {
+ span = "1";
+ }
+ return Integer.parseInt(span);
+ }
+
+ /**
+ * Removes the {@code span} attribute, restoring the default value of 1.
+ */
+ public void resetSpan() {
+ getElement().removeAttribute(ATTRIBUTE_SPAN);
+ }
+}
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableDataCell.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableDataCell.java
new file mode 100644
index 00000000000..67af668f607
--- /dev/null
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableDataCell.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import java.util.List;
+import java.util.Objects;
+
+import com.vaadin.flow.component.ClickNotifier;
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.Tag;
+import com.vaadin.flow.signals.Signal;
+
+/**
+ * Component representing a <td> element (a table data cell).
+ * Inherits {@code colspan}/{@code rowspan} support from {@link TableCell}.
+ *
+ * @since 25.2
+ */
+@Tag(Tag.TD)
+public class TableDataCell extends TableCell
+ implements ClickNotifier {
+
+ /**
+ * Creates a new empty table cell component.
+ */
+ public TableDataCell() {
+ super();
+ }
+
+ /**
+ * Creates a new table cell with the given children components.
+ *
+ * @param components
+ * the children components.
+ */
+ public TableDataCell(Component... components) {
+ super(components);
+ }
+
+ /**
+ * List equivalent of {@link #TableDataCell(Component...)}.
+ *
+ * @param components
+ * the children components.
+ */
+ public TableDataCell(List extends Component> components) {
+ super(components);
+ }
+
+ /**
+ * Creates a new table cell with the given text.
+ *
+ * @param text
+ * the text.
+ */
+ public TableDataCell(String text) {
+ super();
+ setText(text);
+ }
+
+ /**
+ * Creates a new table cell with its text content bound to the given signal.
+ *
+ * While a binding for the text content is active, any attempt to set the
+ * text manually throws
+ * {@link com.vaadin.flow.signals.BindingActiveException}. The same happens
+ * when trying to bind a new Signal while one is already bound.
+ *
+ * Bindings are lifecycle-aware and only active while this component is in
+ * the attached state; they are deactivated while the component is in the
+ * detached state.
+ *
+ * @param textSignal
+ * the signal to bind, not null
+ * @see #bindText(Signal)
+ */
+ public TableDataCell(Signal textSignal) {
+ Objects.requireNonNull(textSignal, "textSignal must not be null");
+ bindText(textSignal);
+ }
+}
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableFoot.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableFoot.java
new file mode 100644
index 00000000000..4acc7fd2967
--- /dev/null
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableFoot.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import java.util.List;
+
+import com.vaadin.flow.component.ClickNotifier;
+import com.vaadin.flow.component.HtmlComponent;
+import com.vaadin.flow.component.Tag;
+
+/**
+ * Component representing a <tfoot> element (the table foot).
+ *
+ * Per the WHATWG
+ * HTML specification, a {@code
} may only contain
+ * <tr> elements. This component therefore extends
+ * {@link HtmlComponent} (rather than
+ * {@link com.vaadin.flow.component.HtmlContainer}) and exposes only
+ * {@link TableRow}-specific operations through {@link TableRowContainer}.
+ *
+ * @since 25.2
+ */
+@Tag(Tag.TFOOT)
+public class TableFoot extends HtmlComponent
+ implements TableRowContainer, ClickNotifier {
+
+ /**
+ * Creates a new empty table foot component.
+ */
+ public TableFoot() {
+ super();
+ }
+
+ /**
+ * Creates a new table foot with the given rows.
+ *
+ * @param rows
+ * the rows to add.
+ */
+ public TableFoot(TableRow... rows) {
+ super();
+ addRows(rows);
+ }
+
+ /**
+ * List equivalent of {@link #TableFoot(TableRow...)}.
+ *
+ * @param rows
+ * the rows to add.
+ */
+ public TableFoot(List extends TableRow> rows) {
+ super();
+ addRows(rows);
+ }
+}
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableHead.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableHead.java
new file mode 100644
index 00000000000..c1da1f6b716
--- /dev/null
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableHead.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import java.util.List;
+
+import com.vaadin.flow.component.ClickNotifier;
+import com.vaadin.flow.component.HtmlComponent;
+import com.vaadin.flow.component.Tag;
+
+/**
+ * Component representing a <thead> element (the table head).
+ *
+ * Per the WHATWG
+ * HTML specification, a {@code } may only contain
+ * <tr> elements. This component therefore extends
+ * {@link HtmlComponent} (rather than
+ * {@link com.vaadin.flow.component.HtmlContainer}) and exposes only
+ * {@link TableRow}-specific operations through {@link TableRowContainer}.
+ *
+ * @since 25.2
+ */
+@Tag(Tag.THEAD)
+public class TableHead extends HtmlComponent
+ implements TableRowContainer, ClickNotifier {
+
+ /**
+ * Creates a new empty table head component.
+ */
+ public TableHead() {
+ super();
+ }
+
+ /**
+ * Creates a new table head with the given rows.
+ *
+ * @param rows
+ * the rows to add.
+ */
+ public TableHead(TableRow... rows) {
+ super();
+ addRows(rows);
+ }
+
+ /**
+ * List equivalent of {@link #TableHead(TableRow...)}.
+ *
+ * @param rows
+ * the rows to add.
+ */
+ public TableHead(List extends TableRow> rows) {
+ super();
+ addRows(rows);
+ }
+}
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableHeaderCell.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableHeaderCell.java
new file mode 100644
index 00000000000..54c980710ee
--- /dev/null
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableHeaderCell.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import com.vaadin.flow.component.ClickNotifier;
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.Tag;
+import com.vaadin.flow.signals.Signal;
+
+/**
+ * Component representing a <th> element — a cell that labels
+ * a group of other cells in a {@link Table}. The exact group is defined by the
+ * {@link #setScope(Scope) scope} attribute (which row, column, row group, or
+ * column group the header applies to) and/or by
+ * {@link TableCell#setHeaders(String...) headers} attributes on the data cells
+ * that reference this header by id.
+ *
+ * Inherits {@code colspan}, {@code rowspan} and {@code headers} support from
+ * {@link TableCell}, since those attributes apply equally to
+ * <td> and <th>.
+ *
+ * @see MDN:
+ * <th> — The Table Header element
+ * @since 25.2
+ */
+@Tag(Tag.TH)
+public class TableHeaderCell extends TableCell
+ implements ClickNotifier {
+
+ /**
+ * Creates a new empty header cell component.
+ */
+ public TableHeaderCell() {
+ super();
+ }
+
+ /**
+ * Creates a new header cell with the given children components.
+ *
+ * @param components
+ * the children components.
+ */
+ public TableHeaderCell(Component... components) {
+ super(components);
+ }
+
+ /**
+ * List equivalent of {@link #TableHeaderCell(Component...)}.
+ *
+ * @param components
+ * the children components.
+ */
+ public TableHeaderCell(List extends Component> components) {
+ super(components);
+ }
+
+ /**
+ * Creates a new header cell with the given text.
+ *
+ * @param text
+ * the text.
+ */
+ public TableHeaderCell(String text) {
+ super();
+ setText(text);
+ }
+
+ /**
+ * Creates a new header cell with its text content bound to the given
+ * signal.
+ *
+ * @param textSignal
+ * the signal to bind, not {@code null}
+ * @see #bindText(Signal)
+ */
+ public TableHeaderCell(Signal textSignal) {
+ Objects.requireNonNull(textSignal, "textSignal must not be null");
+ bindText(textSignal);
+ }
+
+ /**
+ * Defines the cells that a <th> header relates to —
+ * effectively the answer to "which other cells does this header label?".
+ * Setting it correctly is what lets a screen reader read out the right
+ * column or row header when a user navigates into a data cell.
+ *
+ * If no scope is set (or the value is unrecognized), browsers automatically
+ * infer the scope from the table structure. Per the HTML
+ * spec, the standard keywords are {@link #ROW row}, {@link #COL col},
+ * {@link #ROWGROUP rowgroup}, and {@link #COLGROUP colgroup}; {@link #AUTO
+ * auto} writes the literal {@code "auto"} attribute value, which browsers
+ * treat the same as omitting the attribute.
+ */
+ public enum Scope {
+ /**
+ * The header relates to all cells of the row it belongs to. Use this
+ * for a leading <th> that labels its row (e.g. the
+ * row's name or category).
+ */
+ ROW("row"),
+ /**
+ * The header relates to all cells of the column it belongs to. Use this
+ * for a header at the top of a column (the most common case in
+ * <thead>).
+ */
+ COL("col"),
+ /**
+ * The header belongs to a row group ({@code
} or
+ * {@code }/{@code }) and relates to all cells in that
+ * group.
+ */
+ ROWGROUP("rowgroup"),
+ /**
+ * The header belongs to a column group ({@link TableColumnGroup}) and
+ * relates to all cells in that group.
+ */
+ COLGROUP("colgroup"),
+ /**
+ * Writes the literal {@code "auto"} value. Behaves the same as leaving
+ * the attribute unset — the browser infers the scope from the table
+ * structure.
+ */
+ AUTO("auto");
+
+ private final String value;
+
+ Scope(String value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns the attribute value as it appears in the rendered HTML.
+ */
+ public String getValue() {
+ return value;
+ }
+
+ static Scope fromValue(String value) {
+ if (value == null) {
+ return null;
+ }
+ for (Scope s : values()) {
+ if (s.value.equals(value)) {
+ return s;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Sets the {@code scope} attribute, declaring which cells this header
+ * labels. Critical for accessibility — screen readers use this to announce
+ * the right header when a user navigates into a data cell. Pass
+ * {@code null} to remove the attribute (browsers will then infer scope from
+ * structure).
+ *
+ * @param scope
+ * the scope, or {@code null} to clear the attribute.
+ * @see Scope
+ */
+ public void setScope(Scope scope) {
+ if (scope == null) {
+ getElement().removeAttribute("scope");
+ } else {
+ getElement().setAttribute("scope", scope.getValue());
+ }
+ }
+
+ /**
+ * Returns the {@code scope} attribute value, if set.
+ *
+ * @return the scope wrapped in an {@link Optional}, or empty if unset (or
+ * the value is unrecognized).
+ */
+ public Optional getScope() {
+ return Optional.ofNullable(
+ Scope.fromValue(getElement().getAttribute("scope")));
+ }
+}
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableRow.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableRow.java
new file mode 100644
index 00000000000..0acee79ec8b
--- /dev/null
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableRow.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.vaadin.flow.component.ClickNotifier;
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.HtmlComponent;
+import com.vaadin.flow.component.Tag;
+
+/**
+ * Component representing a <tr> element.
+ *
+ * Per the
+ * WHATWG HTML
+ * specification, a {@code
} may only contain {@code | } or
+ * {@code | } elements. This component therefore extends
+ * {@link HtmlComponent} (rather than
+ * {@link com.vaadin.flow.component.HtmlContainer}) so the structural
+ * invariant is preserved.
+ *
+ * The {@link #TableRow(Component...) constructor} and {@link #addCells} accept
+ * any {@link Component}: {@link TableCell} subclasses ({@link TableDataCell},
+ * {@link TableHeaderCell}) are placed as-is, and any other component is
+ * automatically wrapped in a new {@link TableDataCell}.
+ *
+ * @since 25.2
+ */
+@Tag(Tag.TR)
+public class TableRow extends HtmlComponent
+ implements ClickNotifier {
+
+ /**
+ * Creates a new empty table row component.
+ */
+ public TableRow() {
+ super();
+ }
+
+ /**
+ * Creates a new table row with the given children. Any {@link TableCell}
+ * argument ({@link TableDataCell} or {@link TableHeaderCell}) is added
+ * as-is; any other component is wrapped in a new {@link TableDataCell}
+ * — convenient for building rows from arbitrary content without the
+ * boilerplate of explicit {@code new TableDataCell(...)} wrappers.
+ *
+ * @param components
+ * the cells (used as-is) or other components (wrapped in
+ * {@code | }) to place in this row.
+ */
+ public TableRow(Component... components) {
+ this(Arrays.asList(components));
+ }
+
+ /**
+ * List equivalent of {@link #TableRow(Component...)}.
+ *
+ * @param components
+ * the cells or wrap-target components for this row.
+ */
+ public TableRow(List extends Component> components) {
+ super();
+ appendAsCells(components);
+ }
+
+ /**
+ * Add a header cell to this row.
+ *
+ * @return the new {@code | } element.
+ */
+ public TableHeaderCell addHeaderCell() {
+ TableHeaderCell cell = new TableHeaderCell();
+ getElement().appendChild(cell.getElement());
+ return cell;
+ }
+
+ /**
+ * Add a header cell to this row with the given text content.
+ *
+ * @param text
+ * the text content.
+ * @return the new {@code | } element.
+ */
+ public TableHeaderCell addHeaderCell(String text) {
+ TableHeaderCell cell = new TableHeaderCell(text);
+ getElement().appendChild(cell.getElement());
+ return cell;
+ }
+
+ /**
+ * Add a header cell to this row that labels the row itself, with
+ * {@code scope="row"} set on the resulting {@code | }. This is a
+ * shortcut for the common pattern of using a leading {@code | } as a
+ * row label, which assistive technologies announce as the header for
+ * the data cells in the same row.
+ *
+ * @param text
+ * the text content.
+ * @return the new {@code | } element with {@code scope="row"}.
+ */
+ public TableHeaderCell addRowHeaderCell(String text) {
+ TableHeaderCell cell = new TableHeaderCell(text);
+ cell.setScope(TableHeaderCell.Scope.ROW);
+ getElement().appendChild(cell.getElement());
+ return cell;
+ }
+
+ /**
+ * Insert a new header cell into a given position.
+ *
+ * @param position
+ * the position into which the header cell must be added.
+ * @return the new header cell.
+ */
+ public TableHeaderCell insertHeaderCell(int position) {
+ TableHeaderCell headerCell = new TableHeaderCell();
+ getElement().insertChild(position, headerCell.getElement());
+ return headerCell;
+ }
+
+ /**
+ * Add a data cell to this row.
+ *
+ * @return the new {@code | } element.
+ */
+ public TableDataCell addDataCell() {
+ TableDataCell cell = new TableDataCell();
+ getElement().appendChild(cell.getElement());
+ return cell;
+ }
+
+ /**
+ * Add a data cell to this row with the given text content.
+ *
+ * @param text
+ * the text content.
+ * @return the new {@code | } element.
+ */
+ public TableDataCell addDataCell(String text) {
+ TableDataCell cell = new TableDataCell(text);
+ getElement().appendChild(cell.getElement());
+ return cell;
+ }
+
+ /**
+ * Insert a new data cell into a given position.
+ *
+ * @param position
+ * the position into which the data cell must be added.
+ * @return the new data cell.
+ */
+ public TableDataCell insertDataCell(int position) {
+ TableDataCell tableCell = new TableDataCell();
+ getElement().insertChild(position, tableCell.getElement());
+ return tableCell;
+ }
+
+ /**
+ * Returns a list of all header cells in this row.
+ *
+ * @return A list of all header cells in this row.
+ */
+ public List getHeaderCells() {
+ return getChildren().filter(c -> c instanceof TableHeaderCell)
+ .map(c -> (TableHeaderCell) c).collect(Collectors.toList());
+ }
+
+ /**
+ * Returns a list of all data cells in this row.
+ *
+ * @return A list of all data cells in this row.
+ */
+ public List getDataCells() {
+ return getChildren().filter(c -> c instanceof TableDataCell)
+ .map(c -> (TableDataCell) c).collect(Collectors.toList());
+ }
+
+ /**
+ * Returns all cells in this row, in document order — both
+ * {@link TableDataCell} and {@link TableHeaderCell} entries combined. For
+ * kind-specific lists use {@link #getDataCells()} or
+ * {@link #getHeaderCells()}; index into any of these lists with
+ * {@code .get(i)}.
+ *
+ * @return a list of all cells in this row.
+ */
+ public List getCells() {
+ return getChildren().filter(c -> c instanceof TableCell)
+ .map(c -> (TableCell) c).collect(Collectors.toList());
+ }
+
+ /**
+ * Removes a cell from this row.
+ *
+ * @param cell
+ * the cell to remove.
+ */
+ public void removeCell(TableCell cell) {
+ getElement().removeChild(cell.getElement());
+ }
+
+ /**
+ * Appends a sequence of data cells ({@code }) with the given text
+ * contents to this row.
+ *
+ * @param cellTexts
+ * the text content for each data cell.
+ * @return this row, for fluent chaining.
+ */
+ public TableRow addDataCells(String... cellTexts) {
+ return addDataCells(Arrays.asList(cellTexts));
+ }
+
+ /**
+ * List equivalent of {@link #addDataCells(String...)}.
+ *
+ * @param cellTexts
+ * the text content for each data cell.
+ * @return this row, for fluent chaining.
+ */
+ public TableRow addDataCells(List cellTexts) {
+ for (String text : cellTexts) {
+ addDataCell(text);
+ }
+ return this;
+ }
+
+ /**
+ * Appends a sequence of header cells ({@code }) with the given text
+ * contents to this row.
+ *
+ * @param cellTexts
+ * the text content for each header cell.
+ * @return this row, for fluent chaining.
+ */
+ public TableRow addHeaderCells(String... cellTexts) {
+ return addHeaderCells(Arrays.asList(cellTexts));
+ }
+
+ /**
+ * List equivalent of {@link #addHeaderCells(String...)}.
+ *
+ * @param cellTexts
+ * the text content for each header cell.
+ * @return this row, for fluent chaining.
+ */
+ public TableRow addHeaderCells(List cellTexts) {
+ for (String text : cellTexts) {
+ addHeaderCell(text);
+ }
+ return this;
+ }
+
+ /**
+ * Appends children to this row. Any {@link TableCell} argument
+ * ({@link TableDataCell} or {@link TableHeaderCell}) is added as-is;
+ * any other component is wrapped in a new {@link TableDataCell}.
+ *
+ * @param components
+ * the cells (used as-is) or other components (wrapped in
+ * {@code }) to append.
+ * @return this row, for fluent chaining.
+ */
+ public TableRow addCells(Component... components) {
+ return addCells(Arrays.asList(components));
+ }
+
+ /**
+ * List equivalent of {@link #addCells(Component...)}.
+ *
+ * @param components
+ * the cells or wrap-target components.
+ * @return this row, for fluent chaining.
+ */
+ public TableRow addCells(List extends Component> components) {
+ appendAsCells(components);
+ return this;
+ }
+
+ private void appendAsCells(Iterable extends Component> components) {
+ for (Component c : components) {
+ TableCell cell = (c instanceof TableCell tc) ? tc
+ : new TableDataCell(c);
+ getElement().appendChild(cell.getElement());
+ }
+ }
+
+}
diff --git a/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableRowContainer.java b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableRowContainer.java
new file mode 100644
index 00000000000..797344c685c
--- /dev/null
+++ b/flow-html-components/src/main/java/com/vaadin/flow/component/html/TableRowContainer.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.vaadin.flow.component.Component;
+import com.vaadin.flow.component.HasElement;
+
+/**
+ * A container of <tr> elements. Implemented by
+ * {@link TableHead}, {@link TableBody} and {@link TableFoot}.
+ *
+ * Only {@link TableRow} children are accepted, matching the WHATWG HTML
+ * structural rules for {@code }, {@code | } and {@code }.
+ *
+ * Implementers must also be {@link Component} instances.
+ *
+ * @since 25.2
+ */
+interface TableRowContainer extends HasElement {
+
+ private Stream rowComponents() {
+ if (this instanceof Component component) {
+ return component.getChildren();
+ }
+ throw new UnsupportedOperationException(
+ "TableRowContainer must be implemented by a Component");
+ }
+
+ /**
+ * Returns a list of all the rows.
+ *
+ * @return all the rows in the container.
+ */
+ default List getRows() {
+ return rowComponents().filter(c -> c instanceof TableRow)
+ .map(c -> (TableRow) c).collect(Collectors.toList());
+ }
+
+ /**
+ * Appends a list of rows to the container.
+ *
+ * @param rows
+ * the rows to append.
+ */
+ default void addRows(TableRow... rows) {
+ addRows(Arrays.asList(rows));
+ }
+
+ /**
+ * List equivalent of {@link #addRows(TableRow...)}.
+ *
+ * @param rows
+ * the rows to append.
+ */
+ default void addRows(List extends TableRow> rows) {
+ for (TableRow row : rows) {
+ getElement().appendChild(row.getElement());
+ }
+ }
+
+ /**
+ * Create and append a row to the end of the container.
+ *
+ * @return the new row.
+ */
+ default TableRow addRow() {
+ TableRow row = new TableRow();
+ getElement().appendChild(row.getElement());
+ return row;
+ }
+
+ /**
+ * Create and insert a row at a given position.
+ *
+ * @param position
+ * a value greater than or equal to 0 and less than or equal to
+ * the container's size.
+ * @return the new row.
+ */
+ default TableRow insertRow(int position) {
+ TableRow row = new TableRow();
+ getElement().insertChild(position, row.getElement());
+ return row;
+ }
+
+ /**
+ * Remove a list of rows from the container.
+ *
+ * @param rows
+ * the rows to remove.
+ */
+ default void removeRows(TableRow... rows) {
+ removeRows(Arrays.asList(rows));
+ }
+
+ /**
+ * List equivalent of {@link #removeRows(TableRow...)}.
+ *
+ * @param rows
+ * the rows to remove.
+ */
+ default void removeRows(List extends TableRow> rows) {
+ for (TableRow row : rows) {
+ getElement().removeChild(row.getElement());
+ }
+ }
+
+ /**
+ * Remove all the rows in the container.
+ */
+ default void removeAllRows() {
+ getElement().removeAllChildren();
+ }
+
+ /**
+ * Replaces the row at a given position with a new one.
+ *
+ * @param index
+ * the index of the row to replace.
+ * @param row
+ * the new row to insert at the position of the old row.
+ */
+ default void replaceRow(int index, TableRow row) {
+ getElement().setChild(index, row.getElement());
+ }
+
+}
diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/HtmlComponentSmokeTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/HtmlComponentSmokeTest.java
index 86c901d6e8f..88401b21a64 100644
--- a/flow-html-components/src/test/java/com/vaadin/flow/component/html/HtmlComponentSmokeTest.java
+++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/HtmlComponentSmokeTest.java
@@ -75,8 +75,10 @@ class HtmlComponentSmokeTest {
testValues.put(IFrame.SandboxType[].class,
new IFrame.SandboxType[] { IFrame.SandboxType.ALLOW_POPUPS,
IFrame.SandboxType.ALLOW_MODALS });
+ testValues.put(String[].class, new String[] { "a", "b" });
testValues.put(Component.class, new Paragraph("Component"));
testValues.put(HasText.WhiteSpace.class, HasText.WhiteSpace.PRE_LINE);
+ testValues.put(TableHeaderCell.Scope.class, TableHeaderCell.Scope.COL);
}
private static final Map, Map, Object>> specialTestValues = new HashMap<>();
@@ -240,12 +242,25 @@ private static boolean isSpecialSetter(Method method) {
return true;
}
- // NativeTable delegates caption text to the nested element
- if (method.getDeclaringClass() == NativeTable.class
+ // NativeTable / Table delegates caption text to the nested
+ // element
+ if ((method.getDeclaringClass() == NativeTable.class
+ || method.getDeclaringClass() == Table.class)
&& method.getName().startsWith("setCaptionText")) {
return true;
}
+ // TableCell.setHeaders has multiple overloads (String..., String list,
+ // TableHeaderCell...). The String[] variant is exercised normally to
+ // cover the bean property; the others have non-matching getter types
+ // or no matching getter and are covered by focused unit tests.
+ if (method.getDeclaringClass() == TableCell.class && (method.getName()
+ .equals("setHeadersByCells")
+ || (method.getName().equals("setHeaders")
+ && method.getParameterTypes()[0] != String[].class))) {
+ return true;
+ }
+
if (method.getDeclaringClass() == FieldSet.class
&& method.getName().startsWith("setContent")) {
return true;
@@ -408,7 +423,8 @@ private static boolean isClassFile(Path path) {
}
private static boolean isHtmlComponentSubclass(Class> cls) {
- return HtmlComponent.class.isAssignableFrom(cls);
+ return HtmlComponent.class.isAssignableFrom(cls)
+ && !Modifier.isAbstract(cls.getModifiers());
}
private static Class extends HtmlComponent> asHtmlComponentSubclass(
diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableBodyTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableBodyTest.java
new file mode 100644
index 00000000000..dc91a4d05c9
--- /dev/null
+++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableBodyTest.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+public class TableBodyTest extends ComponentTest {
+ // Actual test methods in super class
+
+ @Override
+ protected void addProperties() {
+ // Component defines no new properties
+ }
+}
diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableCaptionTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableCaptionTest.java
new file mode 100644
index 00000000000..783f71d5e53
--- /dev/null
+++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableCaptionTest.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+public class TableCaptionTest extends ComponentTest {
+ // Actual test methods in super class
+
+ @Override
+ protected void addProperties() {
+ // Component defines no new properties
+ }
+}
diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableColumnGroupTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableColumnGroupTest.java
new file mode 100644
index 00000000000..98f98d0640f
--- /dev/null
+++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableColumnGroupTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TableColumnGroupTest extends ComponentTest {
+ // Property tests in super class
+
+ @Override
+ protected void addProperties() {
+ addProperty("span", int.class, 1, 2, false, false);
+ }
+
+ @Test
+ void addColumn_appendsChild() {
+ TableColumnGroup group = (TableColumnGroup) getComponent();
+ TableColumn col = group.addColumn();
+ assertEquals(1, group.getColumns().size());
+ assertEquals(group, col.getParent().orElseThrow());
+ }
+
+ @Test
+ void addColumnWithSpan_appendsChildWithSpan() {
+ TableColumnGroup group = (TableColumnGroup) getComponent();
+ TableColumn col = group.addColumn(2);
+ assertEquals(2, col.getSpan());
+ assertEquals(1, group.getColumns().size());
+ }
+
+ @Test
+ void addColumns_appendsExistingColumns() {
+ TableColumnGroup group = (TableColumnGroup) getComponent();
+ TableColumn c1 = new TableColumn();
+ TableColumn c2 = new TableColumn(3);
+ group.addColumns(c1, c2);
+ List columns = group.getColumns();
+ assertEquals(2, columns.size());
+ assertEquals(c1, columns.get(0));
+ assertEquals(c2, columns.get(1));
+ }
+
+ @Test
+ void varargsConstructor() {
+ TableColumn c1 = new TableColumn();
+ TableColumn c2 = new TableColumn(2);
+ TableColumnGroup group = new TableColumnGroup(c1, c2);
+ assertEquals(2, group.getColumns().size());
+ }
+
+ @Test
+ void removeColumn() {
+ TableColumnGroup group = (TableColumnGroup) getComponent();
+ TableColumn c = group.addColumn();
+ group.removeColumn(c);
+ assertTrue(group.getColumns().isEmpty());
+ assertTrue(c.getParent().isEmpty());
+ }
+
+ @Test
+ void removeAllColumns() {
+ TableColumnGroup group = (TableColumnGroup) getComponent();
+ group.addColumn();
+ group.addColumn();
+ group.addColumn();
+ group.removeAllColumns();
+ assertTrue(group.getColumns().isEmpty());
+ }
+
+ @Test
+ void setSpan_writesAttribute() {
+ TableColumnGroup group = (TableColumnGroup) getComponent();
+ group.setSpan(4);
+ assertEquals("4", group.getElement().getAttribute("span"));
+ assertEquals(4, group.getSpan());
+ }
+
+ @Test
+ void setSpan_rejectsNonPositive() {
+ TableColumnGroup group = (TableColumnGroup) getComponent();
+ assertThrows(IllegalArgumentException.class, () -> group.setSpan(0));
+ }
+
+ @Test
+ void resetSpan_removesAttribute() {
+ TableColumnGroup group = (TableColumnGroup) getComponent();
+ group.setSpan(2);
+ group.resetSpan();
+ assertNull(group.getElement().getAttribute("span"));
+ }
+}
diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableColumnTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableColumnTest.java
new file mode 100644
index 00000000000..ed6ac6c17e4
--- /dev/null
+++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableColumnTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class TableColumnTest extends ComponentTest {
+ // Actual test methods in super class
+
+ @Override
+ protected void addProperties() {
+ addProperty("span", int.class, 1, 2, false, false);
+ }
+
+ @Test
+ void defaultSpanIsOne() {
+ TableColumn col = (TableColumn) getComponent();
+ assertEquals(1, col.getSpan());
+ }
+
+ @Test
+ void setSpan_writesAttribute() {
+ TableColumn col = (TableColumn) getComponent();
+ col.setSpan(3);
+ assertEquals("3", col.getElement().getAttribute("span"));
+ assertEquals(3, col.getSpan());
+ }
+
+ @Test
+ void setSpan_rejectsNonPositive() {
+ TableColumn col = (TableColumn) getComponent();
+ assertThrows(IllegalArgumentException.class, () -> col.setSpan(0));
+ assertThrows(IllegalArgumentException.class, () -> col.setSpan(-1));
+ }
+
+ @Test
+ void resetSpan_removesAttribute() {
+ TableColumn col = (TableColumn) getComponent();
+ col.setSpan(4);
+ col.resetSpan();
+ assertNull(col.getElement().getAttribute("span"));
+ assertEquals(1, col.getSpan());
+ }
+
+ @Test
+ void spanConstructor() {
+ TableColumn col = new TableColumn(5);
+ assertEquals(5, col.getSpan());
+ }
+}
diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableDataCellBindTextTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableDataCellBindTextTest.java
new file mode 100644
index 00000000000..f83bec55817
--- /dev/null
+++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableDataCellBindTextTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import org.junit.jupiter.api.Test;
+
+import com.vaadin.flow.component.UI;
+import com.vaadin.flow.dom.SignalsUnitTest;
+import com.vaadin.flow.signals.BindingActiveException;
+import com.vaadin.flow.signals.local.ValueSignal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class TableDataCellBindTextTest extends SignalsUnitTest {
+
+ @Test
+ void bindText_updatesTextOnSignalChange() {
+ TableDataCell cell = new TableDataCell();
+ UI.getCurrent().add(cell);
+
+ ValueSignal signal = new ValueSignal<>("");
+ cell.bindText(signal);
+
+ signal.set("text-1");
+ assertEquals("text-1", cell.getText());
+
+ signal.set("text-2");
+ assertEquals("text-2", cell.getText());
+ }
+
+ @Test
+ void bindText_setTextWhileBindingActive_throws() {
+ TableDataCell cell = new TableDataCell();
+ UI.getCurrent().add(cell);
+
+ ValueSignal signal = new ValueSignal<>("initial");
+ cell.bindText(signal);
+
+ assertThrows(BindingActiveException.class,
+ () -> cell.setText("manual"));
+ }
+
+ @Test
+ void bindText_nullSignal_throwsNPE() {
+ TableDataCell cell = new TableDataCell();
+ UI.getCurrent().add(cell);
+
+ assertThrows(NullPointerException.class, () -> cell.bindText(null));
+ }
+
+ @Test
+ void constructorWithSignal_bindsText() {
+ ValueSignal signal = new ValueSignal<>("initial");
+ TableDataCell cell = new TableDataCell(signal);
+ UI.getCurrent().add(cell);
+
+ assertEquals("initial", cell.getText());
+
+ signal.set("updated");
+ assertEquals("updated", cell.getText());
+ }
+}
diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableDataCellTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableDataCellTest.java
new file mode 100644
index 00000000000..72f213f266b
--- /dev/null
+++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableDataCellTest.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import org.junit.jupiter.api.Test;
+
+import static com.vaadin.flow.component.html.AssertUtils.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class TableDataCellTest extends ComponentTest {
+ // Actual test methods in super class
+
+ @Override
+ protected void addProperties() {
+ addProperty("colspan", int.class, 1, 2, false, false);
+ addProperty("rowspan", int.class, 1, 2, false, false);
+ addProperty("headers", String[].class, null, new String[] { "a", "b" },
+ true, true);
+ }
+
+ @Test
+ void colspanMustBeNonNegative() {
+ TableDataCell cell = (TableDataCell) getComponent();
+ assertThrows(IllegalArgumentException.class, () -> cell.setColspan(-1));
+ }
+
+ @Test
+ void setColspan() {
+ TableDataCell cell = (TableDataCell) getComponent();
+ cell.setColspan(2);
+ assertEquals("2", cell.getElement().getAttribute("colspan"),
+ "Colspan should be 2");
+ }
+
+ @Test
+ void getDefaultColspan() {
+ TableDataCell cell = (TableDataCell) getComponent();
+ int colspan = cell.getColspan();
+ assertEquals(1, colspan, "Default colspan should be 1");
+ }
+
+ @Test
+ void getColspan() {
+ TableDataCell cell = (TableDataCell) getComponent();
+ cell.getElement().setAttribute("colspan", "2");
+ assertEquals(2, cell.getColspan(), "Colspan should be 2");
+ }
+
+ @Test
+ void resetColspan() {
+ TableDataCell cell = (TableDataCell) getComponent();
+ cell.getElement().setAttribute("colspan", "2");
+ cell.resetColspan();
+ assertNull(cell.getElement().getAttribute("colspan"),
+ "Element should not have colspan attribute");
+ }
+
+ @Test
+ void rowspanMustNonNegative() {
+ TableDataCell cell = (TableDataCell) getComponent();
+ assertThrows(IllegalArgumentException.class, () -> cell.setRowspan(-1));
+ }
+
+ @Test
+ void setRowspan() {
+ TableDataCell cell = (TableDataCell) getComponent();
+ cell.setRowspan(2);
+ assertEquals("2", cell.getElement().getAttribute("rowspan"),
+ "Rowspan should be 2");
+ }
+
+ @Test
+ void getDefaultRowspan() {
+ TableDataCell cell = (TableDataCell) getComponent();
+ int rowspan = cell.getRowspan();
+ assertEquals(1, rowspan, "Default rowspan should be 1");
+ }
+
+ @Test
+ void getRowspan() {
+ TableDataCell cell = (TableDataCell) getComponent();
+ cell.getElement().setAttribute("rowspan", "2");
+ assertEquals(2, cell.getRowspan(), "Rowspan should be 2");
+ }
+
+ @Test
+ void resetRowspan() {
+ TableDataCell cell = (TableDataCell) getComponent();
+ cell.setRowspan(2);
+ cell.resetRowspan();
+ assertNull(cell.getElement().getAttribute("rowspan"),
+ "Element should not have rowspan attribute");
+ }
+
+ @Test
+ void headers_unsetByDefault() {
+ TableDataCell cell = (TableDataCell) getComponent();
+ org.junit.jupiter.api.Assertions
+ .assertTrue(cell.getHeaders().isEmpty());
+ }
+
+ @Test
+ void setHeaders_writesSpaceJoinedAttribute() {
+ TableDataCell cell = (TableDataCell) getComponent();
+ cell.setHeaders("name", "age");
+ assertEquals("name age", cell.getElement().getAttribute("headers"),
+ "headers attribute should be space-joined");
+ org.junit.jupiter.api.Assertions.assertArrayEquals(
+ new String[] { "name", "age" },
+ cell.getHeaders().orElseThrow());
+ }
+
+ @Test
+ void setHeaders_emptyClearsAttribute() {
+ TableDataCell cell = (TableDataCell) getComponent();
+ cell.setHeaders("name");
+ cell.setHeaders(new String[0]);
+ assertNull(cell.getElement().getAttribute("headers"),
+ "Empty array should clear the attribute");
+ org.junit.jupiter.api.Assertions
+ .assertTrue(cell.getHeaders().isEmpty());
+ }
+
+ @Test
+ void setHeaders_fromHeaderCells_resolvesIds() {
+ TableDataCell cell = (TableDataCell) getComponent();
+ TableHeaderCell h1 = new TableHeaderCell("Name");
+ TableHeaderCell h2 = new TableHeaderCell("Age");
+ h1.setId("name-h");
+ h2.setId("age-h");
+ cell.setHeaders(h1, h2);
+ assertEquals("name-h age-h", cell.getElement().getAttribute("headers"),
+ "headers attribute should reference the cells' ids");
+ }
+
+ @Test
+ void setHeaders_fromHeaderCells_throwsIfMissingId() {
+ TableDataCell cell = (TableDataCell) getComponent();
+ TableHeaderCell h1 = new TableHeaderCell("Name");
+ // No id set
+ assertThrows(IllegalArgumentException.class, () -> cell.setHeaders(h1));
+ }
+
+ @Test
+ void resetHeaders_removesAttribute() {
+ TableDataCell cell = (TableDataCell) getComponent();
+ cell.setHeaders("name");
+ cell.resetHeaders();
+ assertNull(cell.getElement().getAttribute("headers"));
+ }
+
+}
diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableFootTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableFootTest.java
new file mode 100644
index 00000000000..b34c4fe98d3
--- /dev/null
+++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableFootTest.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+public class TableFootTest extends ComponentTest {
+ // Actual test methods in super class
+
+ @Override
+ protected void addProperties() {
+ // Component defines no new properties
+ }
+}
diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableHeadTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableHeadTest.java
new file mode 100644
index 00000000000..3588c8c7cf4
--- /dev/null
+++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableHeadTest.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+public class TableHeadTest extends ComponentTest {
+ // Actual test methods in super class
+
+ @Override
+ protected void addProperties() {
+ // Component defines no new properties
+ }
+}
diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableHeaderCellTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableHeaderCellTest.java
new file mode 100644
index 00000000000..3500cbc3b26
--- /dev/null
+++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableHeaderCellTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import java.beans.IntrospectionException;
+import java.lang.reflect.InvocationTargetException;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TableHeaderCellTest extends ComponentTest {
+ // Most tests in super class
+
+ @BeforeEach
+ @Override
+ void setup() throws IntrospectionException, InstantiationException,
+ IllegalAccessException, ClassNotFoundException,
+ InvocationTargetException, NoSuchMethodException {
+ whitelistProperty("scope");
+ super.setup();
+ }
+
+ @Override
+ protected void addProperties() {
+ super.addProperties();
+ // Inherited from TableCell — same semantics as TableDataCell
+ addProperty("colspan", int.class, 1, 2, false, false);
+ addProperty("rowspan", int.class, 1, 2, false, false);
+ addProperty("headers", String[].class, null, new String[] { "a", "b" },
+ true, true);
+ }
+
+ @Test
+ void scope_unsetByDefault() {
+ TableHeaderCell th = (TableHeaderCell) getComponent();
+ assertTrue(th.getScope().isEmpty());
+ }
+
+ @Test
+ void scope_setAndGet() {
+ TableHeaderCell th = (TableHeaderCell) getComponent();
+ th.setScope(TableHeaderCell.Scope.COL);
+ assertEquals(TableHeaderCell.Scope.COL, th.getScope().orElseThrow());
+ assertEquals("col", th.getElement().getAttribute("scope"));
+ }
+
+ @Test
+ void scope_setNullClearsAttribute() {
+ TableHeaderCell th = (TableHeaderCell) getComponent();
+ th.setScope(TableHeaderCell.Scope.ROW);
+ th.setScope(null);
+ assertTrue(th.getScope().isEmpty());
+ assertEquals(null, th.getElement().getAttribute("scope"));
+ }
+
+ @Test
+ void colspan_inheritedFromTableCell() {
+ TableHeaderCell th = (TableHeaderCell) getComponent();
+ assertEquals(1, th.getColspan());
+ th.setColspan(3);
+ assertEquals(3, th.getColspan());
+ assertEquals("3", th.getElement().getAttribute("colspan"));
+ }
+
+ @Test
+ void rowspan_inheritedFromTableCell() {
+ TableHeaderCell th = (TableHeaderCell) getComponent();
+ assertEquals(1, th.getRowspan());
+ th.setRowspan(2);
+ assertEquals(2, th.getRowspan());
+ assertEquals("2", th.getElement().getAttribute("rowspan"));
+ }
+}
diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableRowContainerTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableRowContainerTest.java
new file mode 100644
index 00000000000..c706f9550a7
--- /dev/null
+++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableRowContainerTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TableRowContainerTest {
+
+ private TableBody container;
+
+ @BeforeEach
+ void setUp() {
+ container = new TableBody();
+ }
+
+ @Test
+ void addRow() {
+ var children = container.getElement().getChildren().toList();
+ assertEquals(0, children.size());
+ var row = container.addRow();
+ children = container.getElement().getChildren().toList();
+ assertEquals(1, children.size());
+ AssertUtils.assertEquals(children.get(0), row.getElement(),
+ "Child is not added row");
+ row = container.addRow();
+ children = container.getElement().getChildren().toList();
+ assertEquals(2, children.size());
+ AssertUtils.assertEquals(children.get(1), row.getElement(),
+ "Child is not added row");
+ }
+
+ @Test
+ void getRows() {
+ for (int i = 0; i < 10; i++) {
+ container.addRow();
+ }
+ var rows = container.getRows();
+ assertEquals(10, rows.size());
+ for (int i = 0; i < 10; i++) {
+ AssertUtils.assertEquals(rows.get(i).getElement(),
+ container.getElement().getChild(i), "row does not match");
+ }
+ }
+
+ @Test
+ void insertRow() {
+ var row0 = new TableRow();
+ var row1 = new TableRow();
+ var row2 = new TableRow();
+ container.addRows(row0, row1, row2);
+ var newRow = container.insertRow(1);
+ var rows = container.getRows();
+ assertEquals(4, rows.size());
+ AssertUtils.assertEquals(newRow, rows.get(1),
+ "New row must be inserted at given position");
+ }
+
+ @Test
+ void removeAllRows() {
+ container.addRow();
+ container.addRow();
+ container.addRow();
+ container.removeAllRows();
+ assertEquals(0, container.getRows().size());
+ }
+
+ @Test
+ void removeRowsByReference() {
+ var row0 = container.addRow();
+ var row1 = container.addRow();
+ var row2 = container.addRow();
+ var row3 = container.addRow();
+ var row4 = container.addRow();
+ container.removeRows(row1, row3);
+ assertTrue(row1.getParent().isEmpty());
+ assertTrue(row3.getParent().isEmpty());
+ assertEquals(3, container.getRows().size());
+ AssertUtils.assertEquals(container, row0.getParent().orElseThrow(),
+ "row0 must not be removed");
+ AssertUtils.assertEquals(container, row2.getParent().orElseThrow(),
+ "row2 must not be removed");
+ AssertUtils.assertEquals(container, row4.getParent().orElseThrow(),
+ "row4 must not be removed");
+ }
+
+ @Test
+ void replaceRow() {
+ container.addRow();
+ container.addRow();
+ container.addRow();
+ var newRow = new TableRow();
+ container.replaceRow(1, newRow);
+ var rows = container.getRows();
+ assertEquals(3, rows.size());
+ AssertUtils.assertEquals(newRow, rows.get(1),
+ "Row must be replaced with new row");
+ }
+
+}
diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableRowTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableRowTest.java
new file mode 100644
index 00000000000..69c88664986
--- /dev/null
+++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableRowTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import org.junit.jupiter.api.Test;
+
+import com.vaadin.flow.component.HasComponents;
+import com.vaadin.flow.component.HasOrderedComponents;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+public class TableRowTest extends ComponentTest {
+ // Most tests in super class
+
+ @Override
+ protected void addProperties() {
+ // Component defines no new properties
+ }
+
+ @Test
+ void doesNotExposeGenericAddComponent() {
+ TableRow row = (TableRow) getComponent();
+ assertFalse(row instanceof HasComponents,
+ "TableRow must not implement HasComponents");
+ assertFalse(row instanceof HasOrderedComponents,
+ "TableRow must not implement HasOrderedComponents");
+ }
+
+ @Test
+ void constructor_acceptsCells() {
+ TableDataCell td = new TableDataCell("data");
+ TableHeaderCell th = new TableHeaderCell("hdr");
+ TableRow row = new TableRow(th, td);
+ assertEquals(2, row.getCells().size());
+ assertEquals(1, row.getDataCells().size());
+ assertEquals(1, row.getHeaderCells().size());
+ }
+
+ @Test
+ void addDataCell_appendsTd() {
+ TableRow row = new TableRow();
+ TableDataCell td = row.addDataCell();
+ assertEquals(1, row.getDataCells().size());
+ assertEquals(td, row.getDataCells().get(0));
+ }
+
+ @Test
+ void addHeaderCell_appendsTh() {
+ TableRow row = new TableRow();
+ TableHeaderCell th = row.addHeaderCell("Name");
+ assertEquals(1, row.getHeaderCells().size());
+ assertEquals(th, row.getHeaderCells().get(0));
+ assertEquals("Name", th.getText());
+ }
+
+ @Test
+ void addDataCells_appendsAll() {
+ TableRow row = new TableRow();
+ TableRow result = row.addDataCells("a", "b", "c");
+ assertEquals(row, result);
+ assertEquals(3, row.getDataCells().size());
+ assertEquals("a", row.getDataCells().get(0).getText());
+ assertEquals("b", row.getDataCells().get(1).getText());
+ assertEquals("c", row.getDataCells().get(2).getText());
+ }
+
+ @Test
+ void addHeaderCells_appendsAll() {
+ TableRow row = new TableRow();
+ TableRow result = row.addHeaderCells("Name", "Age");
+ assertEquals(row, result);
+ assertEquals(2, row.getHeaderCells().size());
+ }
+
+ @Test
+ void addCells_appendsPreBuiltCells() {
+ TableRow row = new TableRow();
+ TableHeaderCell th = new TableHeaderCell("Name");
+ TableDataCell td = new TableDataCell("Alice");
+ row.addCells(th, td);
+ assertEquals(1, row.getHeaderCells().size());
+ assertEquals(1, row.getDataCells().size());
+ }
+
+ @Test
+ void constructor_wrapsNonCellComponentsInDataCell() {
+ Span span = new Span("hi");
+ TableHeaderCell th = new TableHeaderCell("Name");
+ TableRow row = new TableRow(span, th);
+
+ assertEquals(2, row.getCells().size());
+ // span got wrapped in a new TableDataCell
+ TableDataCell wrapper = row.getDataCells().get(0);
+ assertEquals(span, wrapper.getChildren().findFirst().orElseThrow());
+ // header cell preserved as-is
+ assertEquals(th, row.getHeaderCells().get(0));
+ }
+
+ @Test
+ void addCells_wrapsNonCellComponentsInDataCell() {
+ TableRow row = new TableRow();
+ Span span = new Span("hi");
+ row.addCells(span);
+
+ assertEquals(1, row.getDataCells().size());
+ assertEquals(span, row.getDataCells().get(0).getChildren().findFirst()
+ .orElseThrow());
+ }
+
+ @Test
+ void addCells_listOverloadMatchesVarargs() {
+ TableRow row = new TableRow();
+ TableDataCell td = new TableDataCell("a");
+ TableHeaderCell th = new TableHeaderCell("h");
+ row.addCells(java.util.List.of(td, th));
+
+ assertEquals(2, row.getCells().size());
+ assertEquals(td, row.getCells().get(0));
+ assertEquals(th, row.getCells().get(1));
+ }
+
+ @Test
+ void addDataCells_listOverloadMatchesVarargs() {
+ TableRow row = new TableRow();
+ row.addDataCells(java.util.List.of("a", "b", "c"));
+ assertEquals(3, row.getDataCells().size());
+ assertEquals("a", row.getDataCells().get(0).getText());
+ assertEquals("c", row.getDataCells().get(2).getText());
+ }
+
+ @Test
+ void removeCell_dropsFromRow() {
+ TableRow row = new TableRow();
+ TableHeaderCell th = row.addHeaderCell("Name");
+ TableDataCell td = row.addDataCell("Alice");
+ row.removeCell(th);
+ assertEquals(1, row.getCells().size());
+ assertEquals(td, row.getCells().get(0));
+ }
+
+ @Test
+ void addRowHeaderCell_setsScopeRow() {
+ TableRow row = new TableRow();
+ TableHeaderCell th = row.addRowHeaderCell("Cucumber");
+ assertEquals("Cucumber", th.getText());
+ assertEquals(TableHeaderCell.Scope.ROW, th.getScope().orElseThrow());
+ assertEquals(1, row.getHeaderCells().size());
+ assertEquals(th, row.getHeaderCells().get(0));
+ }
+}
diff --git a/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableTest.java b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableTest.java
new file mode 100644
index 00000000000..80e6637628d
--- /dev/null
+++ b/flow-html-components/src/test/java/com/vaadin/flow/component/html/TableTest.java
@@ -0,0 +1,454 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.component.html;
+
+import java.beans.IntrospectionException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.vaadin.flow.component.HasComponents;
+import com.vaadin.flow.component.HasOrderedComponents;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TableTest extends ComponentTest {
+ // Actual test methods in super class
+
+ @BeforeEach
+ @Override
+ void setup() throws IntrospectionException, InstantiationException,
+ IllegalAccessException, ClassNotFoundException,
+ InvocationTargetException, NoSuchMethodException {
+ whitelistProperty("captionText");
+ super.setup();
+ }
+
+ @Test
+ void getCaption() {
+ var component = (Table) getComponent();
+ TableCaption caption = component.getCaption();
+ AssertUtils.assertEquals(component.getChildren().toList().get(0),
+ caption, "Caption does not match");
+ }
+
+ @Test
+ void addsCaptionAsFirstChild() {
+ var component = (Table) getComponent();
+ assertEquals(0, component.getChildren().count());
+ component.getHead();
+ component.addBody();
+ component.getFoot();
+ var caption = component.getCaption();
+ assertEquals(4, component.getChildren().count());
+ AssertUtils.assertEquals(caption,
+ component.getChildren().findFirst().orElseThrow(),
+ "Caption is not the first child");
+ AssertUtils.assertEquals(caption.getParent().orElseThrow(), component,
+ "Table is not the caption's father");
+ }
+
+ @Test
+ void setCaptionText() {
+ var component = (Table) getComponent();
+ String expectedText = "Test caption text.";
+ component.setCaptionText(expectedText);
+ var caption = component.getCaption();
+ assertEquals(expectedText, caption.getText());
+ }
+
+ @Test
+ void getCaptionText_emptyWhenNoCaption() {
+ var component = (Table) getComponent();
+ assertEquals("", component.getCaptionText());
+ assertTrue(component.findCaption().isEmpty());
+ }
+
+ @Test
+ void getCaptionText_returnsCaptionText() {
+ var component = (Table) getComponent();
+ String expectedText = "Test caption text.";
+ var caption = component.getCaption();
+ caption.setText(expectedText);
+ assertEquals(expectedText, component.getCaptionText());
+ }
+
+ @Test
+ void removeCaption() {
+ var component = (Table) getComponent();
+ var caption = component.getCaption();
+ component.removeCaption();
+ assertTrue(caption.getParent().isEmpty());
+ assertTrue(component.findCaption().isEmpty());
+ }
+
+ @Test
+ void getHead() {
+ var component = (Table) getComponent();
+ assertEquals(0, component.getChildren().count());
+ TableHead head = component.getHead();
+ AssertUtils.assertEquals(component, head.getParent().orElseThrow(),
+ "head was not added");
+ }
+
+ @Test
+ void addHeadAfterCaption() {
+ var component = (Table) getComponent();
+ component.getCaption();
+ var head = component.getHead();
+ assertEquals(2, component.getChildren().count());
+ int headIndex = component.getChildren().toList().indexOf(head);
+ assertEquals(1, headIndex);
+ }
+
+ @Test
+ void removeHead() {
+ var component = (Table) getComponent();
+ TableHead head = component.getHead();
+ component.removeHead();
+ assertTrue(head.getParent().isEmpty());
+ assertTrue(component.findHead().isEmpty());
+ }
+
+ @Test
+ void getFoot() {
+ var component = (Table) getComponent();
+ assertEquals(0, component.getChildren().count());
+ TableFoot footer = component.getFoot();
+ AssertUtils.assertEquals(component, footer.getParent().orElseThrow(),
+ "footer was not added");
+ }
+
+ @Test
+ void removeFoot() {
+ var component = (Table) getComponent();
+ TableFoot footer = component.getFoot();
+ component.removeFoot();
+ assertTrue(footer.getParent().isEmpty());
+ assertTrue(component.findFoot().isEmpty());
+ }
+
+ @Test
+ void addBody() {
+ var component = (Table) getComponent();
+ component.addBody();
+ assertEquals(1, component.getChildren().count());
+ component.addBody();
+ assertEquals(2, component.getChildren().count());
+ }
+
+ @Test
+ void addBodyAfterCaption() {
+ var component = (Table) getComponent();
+ component.getCaption();
+ var body = component.addBody();
+ assertEquals(1, component.getChildren().toList().indexOf(body));
+ }
+
+ @Test
+ void addBodyAfterHeader() {
+ var component = (Table) getComponent();
+ component.getHead();
+ var body = component.addBody();
+ assertEquals(1, component.getChildren().toList().indexOf(body));
+ }
+
+ @Test
+ void addBodyAfterBothCaptionAndHeader() {
+ var component = (Table) getComponent();
+ component.getCaption();
+ component.getHead();
+ var body = component.addBody();
+ assertEquals(2, component.getChildren().toList().indexOf(body));
+ }
+
+ @Test
+ void addBodyBeforeFoot() {
+ var component = (Table) getComponent();
+ component.getFoot();
+ var body = component.addBody();
+ assertEquals(0, component.getChildren().toList().indexOf(body));
+ assertEquals(1,
+ component.getChildren().toList().indexOf(component.getFoot()));
+ }
+
+ @Test
+ void getBody() {
+ var component = (Table) getComponent();
+ var body = component.getBody();
+ assertEquals(1, component.getChildren().count());
+ component.addBody();
+ assertEquals(2, component.getChildren().count());
+ var secondCallBody = component.getBody();
+ AssertUtils.assertEquals(body, secondCallBody,
+ "No new body should've been created");
+ }
+
+ @Test
+ void getBodies() {
+ var component = (Table) getComponent();
+ for (int i = 0; i < 10; i++) {
+ component.addBody();
+ }
+ List bodies = component.getBodies();
+ for (TableBody body : bodies) {
+ AssertUtils.assertEquals(component, body.getParent().orElseThrow(),
+ "Body is not a child of table");
+ }
+ }
+
+ @Test
+ void removeBodyByReference() {
+ var component = (Table) getComponent();
+ var body0 = component.addBody();
+ var body1 = component.addBody();
+ var body2 = component.addBody();
+ component.removeBody(body1);
+ assertTrue(body0.getParent().isPresent());
+ assertTrue(body1.getParent().isEmpty());
+ assertTrue(body2.getParent().isPresent());
+ }
+
+ @Test
+ void addRow_autoCreatesBody() {
+ var table = (Table) getComponent();
+ TableRow row = table.addRow();
+ assertTrue(table.findHead().isEmpty());
+ assertEquals(1, table.getBodies().size());
+ assertEquals(1, table.getBody().getRows().size());
+ AssertUtils.assertEquals(table.getBody(), row.getParent().orElseThrow(),
+ "row must live inside the auto-created tbody");
+ }
+
+ @Test
+ void addRow_withCellTexts_createsDataCells() {
+ var table = (Table) getComponent();
+ TableRow row = table.addRow("Alice", "30", "Blue");
+ assertEquals(3, row.getDataCells().size());
+ assertEquals("Alice", row.getDataCells().get(0).getText());
+ assertEquals("30", row.getDataCells().get(1).getText());
+ assertEquals("Blue", row.getDataCells().get(2).getText());
+ assertEquals(0, row.getHeaderCells().size());
+ }
+
+ @Test
+ void addHeaderRow_autoCreatesThead() {
+ var table = (Table) getComponent();
+ TableRow row = table.addHeaderRow("Name", "Age");
+ assertTrue(table.findHead().isPresent());
+ assertEquals(1, table.getHead().getRows().size());
+ assertEquals(2, row.getHeaderCells().size());
+ assertEquals("Name", row.getHeaderCells().get(0).getText());
+ }
+
+ @Test
+ void addFooterRow_autoCreatesTfoot() {
+ var table = (Table) getComponent();
+ TableRow row = table.addFooterRow("Total", "55");
+ assertTrue(table.findFoot().isPresent());
+ assertEquals(1, table.getFoot().getRows().size());
+ assertEquals(2, row.getDataCells().size());
+ }
+
+ @Test
+ void mdnTutorialStyleConstruction() {
+ // Mirrors the MDN "HTML table basics" walkthrough: caption, header
+ // row, body rows. Verifies the resulting structure is spec-compliant
+ // (caption first, thead before tbody) and that all rows landed in
+ // the right wrappers.
+ var table = (Table) getComponent();
+ table.setCaptionText("People");
+ table.addHeaderRow("Name", "Age", "Color");
+ table.addRow("Alice", "30", "Blue");
+ table.addRow("Bob", "25", "Green");
+
+ assertEquals("People", table.getCaptionText());
+ assertEquals(1, table.getHead().getRows().size());
+ assertEquals(2, table.getBody().getRows().size());
+
+ var children = table.getChildren().toList();
+ assertEquals(table.getCaption(), children.get(0));
+ assertEquals(table.getHead(), children.get(1));
+ assertEquals(table.getBody(), children.get(2));
+ }
+
+ @Test
+ void addRows_addsExistingRowsToBody() {
+ var table = (Table) getComponent();
+ var r1 = new TableRow();
+ var r2 = new TableRow();
+ table.addRows(r1, r2);
+ assertEquals(2, table.getBody().getRows().size());
+ AssertUtils.assertEquals(table.getBody(), r1.getParent().orElseThrow(),
+ "r1 must be a child of tbody");
+ AssertUtils.assertEquals(table.getBody(), r2.getParent().orElseThrow(),
+ "r2 must be a child of tbody");
+ }
+
+ @Test
+ void addCaption_createsAndAppendsComponents() {
+ var table = (Table) getComponent();
+ var span = new Span("Cars");
+ var caption = table.addCaption(span);
+ assertEquals(1, caption.getComponentCount());
+ assertEquals(span, caption.getComponentAt(0));
+ assertEquals(table.getCaption(), caption);
+ }
+
+ @Test
+ void addColumnGroup_insertedAfterCaptionBeforeHead() {
+ var table = (Table) getComponent();
+ table.getCaption();
+ table.getHead();
+ var group = table.addColumnGroup();
+ var children = table.getChildren().toList();
+ assertEquals(table.getCaption(), children.get(0));
+ assertEquals(group, children.get(1));
+ assertEquals(table.getHead(), children.get(2));
+ }
+
+ @Test
+ void addColumnGroup_beforeHeadEvenIfHeadAddedLater() {
+ var table = (Table) getComponent();
+ var group = table.addColumnGroup();
+ var head = table.getHead();
+ var children = table.getChildren().toList();
+ assertEquals(group, children.get(0));
+ assertEquals(head, children.get(1));
+ }
+
+ @Test
+ void addColumnGroup_withColumns() {
+ var table = (Table) getComponent();
+ var c1 = new TableColumn();
+ var c2 = new TableColumn(2);
+ var group = table.addColumnGroup(c1, c2);
+ assertEquals(2, group.getColumns().size());
+ assertEquals(List.of(group), table.getColumnGroups());
+ }
+
+ @Test
+ void multipleColumnGroups_appearInInsertionOrder() {
+ var table = (Table) getComponent();
+ var g1 = table.addColumnGroup();
+ var g2 = table.addColumnGroup();
+ var children = table.getChildren().toList();
+ assertEquals(g1, children.get(0));
+ assertEquals(g2, children.get(1));
+ }
+
+ @Test
+ void removeColumnGroup() {
+ var table = (Table) getComponent();
+ var g1 = table.addColumnGroup();
+ var g2 = table.addColumnGroup();
+ table.removeColumnGroup(g1);
+ assertEquals(List.of(g2), table.getColumnGroups());
+ assertTrue(g1.getParent().isEmpty());
+ }
+
+ @Test
+ void bodyAppendIndex_accountsForColumnGroups() {
+ // caption + 2 colgroups + thead → tbody must land at index 4
+ var table = (Table) getComponent();
+ table.setCaptionText("x");
+ table.addColumnGroup();
+ table.addColumnGroup();
+ table.getHead();
+ var body = table.addBody();
+ assertEquals(4, table.getChildren().toList().indexOf(body));
+ }
+
+ @Test
+ void getRows_emptyWhenNoSections() {
+ var table = (Table) getComponent();
+ assertTrue(table.getRows().isEmpty());
+ }
+
+ @Test
+ void getRows_returnsHeadBodiesFootInOrder() {
+ var table = (Table) getComponent();
+ var headRow = table.addHeaderRow("Name");
+ var bodyRow1 = table.addRow("Alice");
+ var bodyRow2 = table.addRow("Bob");
+ var footRow = table.addFooterRow("Total");
+
+ var rows = table.getRows();
+ assertEquals(List.of(headRow, bodyRow1, bodyRow2, footRow), rows);
+ }
+
+ @Test
+ void getRows_concatenatesMultipleBodies() {
+ var table = (Table) getComponent();
+ var b1Row = table.getBody().addRow();
+ var b2Row = table.addBody().addRow();
+ var rows = table.getRows();
+ assertEquals(List.of(b1Row, b2Row), rows);
+ }
+
+ @Test
+ void getRows_isUnmodifiable() {
+ var table = (Table) getComponent();
+ table.addRow();
+ var rows = table.getRows();
+ assertThrows(UnsupportedOperationException.class,
+ () -> rows.add(new TableRow()));
+ }
+
+ @Test
+ void removeAllRows_clearsRowsButKeepsSections() {
+ var table = (Table) getComponent();
+ table.addHeaderRow("h");
+ table.addRow("b1");
+ table.addRow("b2");
+ table.addFooterRow("f");
+
+ table.removeAllRows();
+
+ assertTrue(table.getRows().isEmpty());
+ // Sections themselves remain
+ assertTrue(table.findHead().isPresent());
+ assertEquals(1, table.getBodies().size());
+ assertTrue(table.findFoot().isPresent());
+ assertEquals(0, table.getHead().getRows().size());
+ assertEquals(0, table.getBody().getRows().size());
+ assertEquals(0, table.getFoot().getRows().size());
+ }
+
+ @Test
+ void removeAllRows_isNoOpOnEmptyTable() {
+ var table = (Table) getComponent();
+ table.removeAllRows();
+ assertTrue(table.getRows().isEmpty());
+ }
+
+ @Test
+ void doesNotExposeGenericAddComponent() {
+ var component = (Table) getComponent();
+ // The strict Table API must not expose generic add(Component) or
+ // any of the other arbitrary-child container helpers.
+ assertFalse(component instanceof HasComponents,
+ "Table must not implement HasComponents");
+ assertFalse(component instanceof HasOrderedComponents,
+ "Table must not implement HasOrderedComponents");
+ }
+
+}
diff --git a/flow-server/src/main/java/com/vaadin/flow/component/Tag.java b/flow-server/src/main/java/com/vaadin/flow/component/Tag.java
index e122c5eddc3..6a0898f6832 100644
--- a/flow-server/src/main/java/com/vaadin/flow/component/Tag.java
+++ b/flow-server/src/main/java/com/vaadin/flow/component/Tag.java
@@ -66,6 +66,14 @@
* Tag for a <code>.
*/
String CODE = "code";
+ /**
+ * Tag for a <col>.
+ */
+ String COL = "col";
+ /**
+ * Tag for a <colgroup>.
+ */
+ String COLGROUP = "colgroup";
/**
* Tag for an <dd>.
*/
| | | |