diff --git a/src/base/component.rs b/src/base/component.rs
index 6bb4e8b9..687d2995 100644
--- a/src/base/component.rs
+++ b/src/base/component.rs
@@ -34,3 +34,7 @@ pub use pager::{Pager, PagerAlign, PagerVisibility};
mod poweredby;
pub use poweredby::PoweredBy;
+
+pub mod table;
+#[doc(inline)]
+pub use table::Table;
diff --git a/src/base/component/table.rs b/src/base/component/table.rs
new file mode 100644
index 00000000..725c26be
--- /dev/null
+++ b/src/base/component/table.rs
@@ -0,0 +1,16 @@
+//! Definiciones para representar tablas de datos ([`Table`]).
+
+mod props;
+pub use props::SortLink;
+
+mod component;
+pub use component::Table;
+
+mod column;
+pub use column::Column;
+
+mod row;
+pub use row::Row;
+
+mod cell;
+pub use cell::Cell;
diff --git a/src/base/component/table/cell.rs b/src/base/component/table/cell.rs
new file mode 100644
index 00000000..0f49a826
--- /dev/null
+++ b/src/base/component/table/cell.rs
@@ -0,0 +1,114 @@
+use crate::prelude::*;
+
+/// Representa una celda de datos (`
`) de una [`table::Row`].
+///
+/// El contenido es una lista [`Children`] para admitir cualquier número de componentes.
+/// [`Cell::new()`] añade un componente como contenido inicial, normalmente un [`Lc`] con texto
+/// literal ([`Lc::n()`]) o traducible (con [`Lc::l()`]/[`Lc::t()`]).
+///
+/// Un `&str`, un `String` o un [`Lc`] se convierten directamente en `Cell`; para estos tipos no
+/// sería necesario llamar a [`Cell::new()`] de forma explícita.
+///
+/// ```rust,no_run
+/// use pagetop::prelude::*;
+///
+/// let email: table::Cell = "ana@example.com".into();
+/// let label: table::Cell = Lc::l("table-status").into();
+/// let name = table::Cell::new(Html::with(|_| html! { "Julia" }));
+/// ```
+#[derive(AutoDefault, Clone, Debug, Getters)]
+pub struct Cell {
+ /// Devuelve identificador, clases CSS y atributos HTML de la celda (`
`).
+ props: Props,
+ /// Devuelve la lista de componentes hijo que genera el contenido de la celda.
+ children: Children,
+}
+
+impl Cell {
+ /// Crea una celda a partir del componente o texto ([`Lc`]) indicado.
+ ///
+ /// Para combinar varios componentes en la misma celda basta con encadenar llamadas a
+ /// [`with_child()`](Cell::with_child):
+ ///
+ /// ```rust,no_run
+ /// # use pagetop::prelude::*;
+ /// let status = table::Cell::new(Lc::n("Admin ")).with_child(Badge::labeled(Lc::n("active")));
+ /// ```
+ pub fn new(child: impl Into) -> Self {
+ Self {
+ children: Children::with(child.into()),
+ ..Default::default()
+ }
+ }
+
+ // **< Cell BUILDER >***************************************************************************
+
+ /// Establece el identificador único de la celda.
+ #[builder_fn]
+ pub fn with_id(mut self, id: impl Into) -> Self {
+ self.props.alter_id(id);
+ self
+ }
+
+ /// Modifica identificador, clases CSS o atributos HTML de la celda.
+ #[builder_fn]
+ pub fn with_prop(mut self, op: PropsOp) -> Self {
+ self.props.alter_prop(op);
+ self
+ }
+
+ /// Establece el número de columnas que ocupa la celda (atributo `colspan`).
+ ///
+ /// Con `1` (el valor por defecto de HTML) elimina el atributo en vez de fijarlo.
+ #[builder_fn]
+ pub fn with_colspan(mut self, span: u8) -> Self {
+ self.props.alter_prop(if span == 1 {
+ PropsOp::remove("colspan")
+ } else {
+ PropsOp::set("colspan", span.to_string())
+ });
+ self
+ }
+
+ /// Establece el número de filas que ocupa la celda (atributo `rowspan`).
+ ///
+ /// Con `1` (el valor por defecto de HTML) elimina el atributo en vez de fijarlo.
+ #[builder_fn]
+ pub fn with_rowspan(mut self, span: u8) -> Self {
+ self.props.alter_prop(if span == 1 {
+ PropsOp::remove("rowspan")
+ } else {
+ PropsOp::set("rowspan", span.to_string())
+ });
+ self
+ }
+
+ /// Añade un nuevo componente a la celda o modifica la lista de componentes (`children`) con una
+ /// operación [`ChildOp`].
+ #[builder_fn]
+ pub fn with_child(mut self, op: impl Into) -> Self {
+ self.children.alter_child(op.into());
+ self
+ }
+}
+
+impl From<&str> for Cell {
+ /// Convierte un `&str` en una celda de texto literal; equivale a `Cell::new(Lc::n(text))`.
+ fn from(text: &str) -> Self {
+ Cell::new(Lc::n(text.to_string()))
+ }
+}
+
+impl From for Cell {
+ /// Convierte un `String` en una celda de texto literal; equivale a `Cell::new(Lc::n(text))`.
+ fn from(text: String) -> Self {
+ Cell::new(Lc::n(text))
+ }
+}
+
+impl From for Cell {
+ /// Convierte un [`Lc`] en una celda traducible; equivale a `Cell::new(label)`.
+ fn from(label: Lc) -> Self {
+ Cell::new(label)
+ }
+}
diff --git a/src/base/component/table/column.rs b/src/base/component/table/column.rs
new file mode 100644
index 00000000..cc55ca71
--- /dev/null
+++ b/src/base/component/table/column.rs
@@ -0,0 +1,128 @@
+use crate::prelude::*;
+
+/// Cabecera de columna (`
`) de una [`Table`].
+///
+/// El contenido (`label`) es un [`Lc`]. Si la columna debe permitir ordenar la tabla al pulsarla,
+/// añade un [`table::SortLink`] con [`with_sort()`](Self::with_sort) para que `Table` envuelva la
+/// etiqueta en un enlace y añada el `aria-sort` y las clases CSS correspondientes.
+///
+/// Un `&str`, un `String` o un [`Lc`] se convierten directamente en `Column`; para estos tipos no
+/// sería necesario llamar a [`Column::new()`] de forma explícita.
+///
+/// ```rust
+/// use pagetop::locale::Lc;
+/// use pagetop::base::component::table::Column;
+///
+/// let column: Column = Lc::n("User").into();
+/// assert_eq!(column.label().get(), Some("User".to_string()));
+/// assert!(column.sort().is_none());
+/// ```
+#[derive(AutoDefault, Clone, Debug, Getters)]
+pub struct Column {
+ /// Devuelve identificador, clases CSS y atributos HTML de la celda de cabecera (`
`).
+ props: Props,
+ /// Devuelve el contenido de la cabecera.
+ label: Lc,
+ /// Devuelve el enlace de ordenación de la columna, si es ordenable.
+ sort: Option,
+}
+
+impl Column {
+ /// Crea una cabecera con el texto localizado indicado.
+ pub fn new(label: Lc) -> Self {
+ Self {
+ label,
+ ..Default::default()
+ }
+ }
+
+ // **< Column BUILDER >*************************************************************************
+
+ /// Establece el identificador único de la celda de cabecera.
+ #[builder_fn]
+ pub fn with_id(mut self, id: impl Into) -> Self {
+ self.props.alter_id(id);
+ self
+ }
+
+ /// Modifica identificador, clases CSS o atributos HTML de la columna.
+ #[builder_fn]
+ pub fn with_prop(mut self, op: PropsOp) -> Self {
+ self.props.alter_prop(op);
+ self
+ }
+
+ /// Establece el número de columnas que ocupa la cabecera (atributo `colspan`).
+ ///
+ /// Con `1` (el valor por defecto de HTML) elimina el atributo en vez de fijarlo.
+ #[builder_fn]
+ pub fn with_colspan(mut self, span: u8) -> Self {
+ self.props.alter_prop(if span == 1 {
+ PropsOp::remove("colspan")
+ } else {
+ PropsOp::set("colspan", span.to_string())
+ });
+ self
+ }
+
+ /// Establece el número de filas que ocupa la cabecera (atributo `rowspan`).
+ ///
+ /// Con `1` (el valor por defecto de HTML) elimina el atributo en vez de fijarlo.
+ #[builder_fn]
+ pub fn with_rowspan(mut self, span: u8) -> Self {
+ self.props.alter_prop(if span == 1 {
+ PropsOp::remove("rowspan")
+ } else {
+ PropsOp::set("rowspan", span.to_string())
+ });
+ self
+ }
+
+ /// Convierte la columna en ordenable con el enlace indicado, o la vuelve no ordenable con
+ /// `None`.
+ #[builder_fn]
+ pub fn with_sort(mut self, sort: impl Into