Table
Rows of data you can sort and select. Dense records, not marketing layouts.
Editorial
<!-- F12c editorial — non-derivable only. Review: Karen. -->
## Ejemplos
Dense records:
```tsx import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell, TableCaption, } from '@/components/atoms/Table';
<Table> <TableCaption>Invoices</TableCaption> <TableHeader> <TableRow> <TableHead>Customer</TableHead> <TableHead>Amount</TableHead> </TableRow> </TableHeader> <TableBody> <TableRow selected={false}> <TableCell>Ada</TableCell> <TableCell>$120</TableCell> </TableRow> </TableBody> </Table> ```
## Accesibilidad
- Use `TableHead` for column headers and `TableCaption` for a summary. - `selected` lives on `TableRow`, not the root table.
## Cuándo no usar
- Marketing comparison layouts → layout blocks / cards. - Simple key-value pairs → definition list or `Item` rows.
## Criterio de uso
- Usa Table cuando las relaciones entre columnas y filas importan y el usuario necesita comparar registros. - Mantén encabezados descriptivos, una caption útil y estados de selección que se entiendan junto con el contenido de la fila. - Para tablas densas, prioriza lectura y navegación por teclado antes de añadir decoración o acciones por celda.
## Gotchas
- Table es para datos, no para construir layout visual; usarlo como grid de marketing perjudica semántica y responsive. - `selected` pertenece a `TableRow`; no lo apliques al root esperando que comunique una selección global.
Uso
import {
Table, TableHeader, TableBody, TableRow, TableHead, TableCell, TableCaption,
} from '@/components/atoms/Table';
<Table>
<TableCaption>Invoices</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Customer</TableHead>
<TableHead>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Ada</TableCell>
<TableCell>$120</TableCell>
</TableRow>
</TableBody>
</Table>Props
selected: boolean
Gotchas
- react
Compose Table > TableHeader/Body/Footer + TableRow + TableHead/Cell. selected lives on TableRow, not the root table.
- a11y
Use TableHead for column headers and TableCaption for a summary; avoid layout tables.
Anatomía CSS
<div class="table-wrapper"> </div>
| Clase | Propósito |
|---|---|
table-wrapper | root |
Tokens resueltos
Valores finales tras seguir la cadena de tokens. Derivados del source: si un token cambia, esta tabla cambia sola.
| Variante | Prop | Valor | Token |
|---|---|---|---|
| all | fg | #0a0a0a | foreground |
| all | border | 1px | stroke.hairline |
| all | hover-bg | #f5f5f5 | muted |
| all | bg | #f5f5f5 | muted |
Animaciones
| Propiedad | Duración | Easing |
|---|---|---|
background-color | var(--duration-100) | var(--easing-in-out) |
CSS mínimo funcional
Autocontenido: sin imports ni tokens. Para previews y prototipos — en producción se consume el CSS del DS (@atom-uikit/css/components.css</code>, <code>@atom-uikit/tokens/tokens.css).
/* -------------------------------------------------------------------------
Table
Semantic HTML table with consistent styling.
Thin wrapper: Table, TableHeader, TableBody, TableFooter,
TableRow, TableHead, TableCell, TableCaption
Parts: .table, .table__header, .table__body, .table__footer,
.table__row, .table__head, .table__cell, .table__caption
------------------------------------------------------------------------- */
/* ---- Wrapper ---- */
.table-wrapper {
width: 100%;
overflow-x: auto;
}
/* ---- Table ---- */
.table {
width: 100%;
border-collapse: collapse;
font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
font-size: 12.8px;
line-height: 1.45;
text-align: left;
}
/* ---- Caption ---- */
.table__caption {
padding-top: 16px;
font-size: 12.8px;
color: #525252;
caption-side: bottom;
}
/* ---- Header ---- */
.table__header {
border-bottom: 1px solid #e5e5e5;
}
.table__head {
padding: 12px 16px;
font-weight: 500;
color: #525252;
white-space: nowrap;
vertical-align: middle;
height: 40px;
}
/* ---- Body ---- */
.table__row {
border-bottom: 1px solid #e5e5e5;
transition: background-color 100ms cubic-bezier(0.4, 0, 0.2, 1);
}
.table__row:hover {
background-color: #f5f5f5;
}
.table__row--selected {
background-color: #f5f5f5;
}
.table__cell {
padding: 12px 16px;
color: #0a0a0a;
vertical-align: middle;
}
/* ---- Footer ---- */
.table__footer {
border-top: 1px solid #e5e5e5;
background-color: #f5f5f5;
}
.table__footer .table__cell {
font-weight: 600;
}
/* ---- Alignment helpers ---- */
.table__head--right,
.table__cell--right {
text-align: right;
}
.table__head--center,
.table__cell--center {
text-align: center;
}
/* ---- Reduced motion ---- */
@media (prefers-reduced-motion: reduce) {
.table__row {
transition-duration: 0ms;
}
}
Codigo fuente
import { forwardRef, type HTMLAttributes, type TdHTMLAttributes, type ThHTMLAttributes } from 'react'; function cn(...classes: (string | false | undefined | null)[]) { return classes.filter(Boolean).join(' '); } export const Table = forwardRef<HTMLTableElement, HTMLAttributes<HTMLTableElement>>( ({ className, ...props }, ref) => ( <div className="table-wrapper"> <table ref={ref} className={cn('table', className)} {...props} /> </div> ), ); Table.displayName = 'Table'; export const TableHeader = forwardRef<HTMLTableSectionElement, HTMLAttributes<HTMLTableSectionElement>>( ({ className, ...props }, ref) => ( <thead ref={ref} className={cn('table__header', className)} {...props} /> ), ); TableHeader.displayName = 'TableHeader'; export const TableBody = forwardRef<HTMLTableSectionElement, HTMLAttributes<HTMLTableSectionElement>>( ({ className, ...props }, ref) => ( <tbody ref={ref} className={cn('table__body', className)} {...props} /> ), ); TableBody.displayName = 'TableBody'; export const TableFooter = forwardRef<HTMLTableSectionElement, HTMLAttributes<HTMLTableSectionElement>>( ({ className, ...props }, ref) => ( <tfoot ref={ref} className={cn('table__footer', className)} {...props} /> ), ); TableFooter.displayName = 'TableFooter'; export type TableRowProps = { selected?: boolean; } & HTMLAttributes<HTMLTableRowElement>; export const TableRow = forwardRef<HTMLTableRowElement, TableRowProps>( ({ className, selected, ...props }, ref) => ( <tr ref={ref} className={cn('table__row', selected && 'table__row--selected', className)} {...props} /> ), ); TableRow.displayName = 'TableRow'; export const TableHead = forwardRef<HTMLTableCellElement, ThHTMLAttributes<HTMLTableCellElement>>( ({ className, ...props }, ref) => ( <th ref={ref} className={cn('table__head', className)} {...props} /> ), ); TableHead.displayName = 'TableHead'; export const TableCell = forwardRef<HTMLTableCellElement, TdHTMLAttributes<HTMLTableCellElement>>( ({ className, ...props }, ref) => ( <td ref={ref} className={cn('table__cell', className)} {...props} /> ), ); TableCell.displayName = 'TableCell'; export const TableCaption = forwardRef<HTMLTableCaptionElement, HTMLAttributes<HTMLTableCaptionElement>>( ({ className, ...props }, ref) => ( <caption ref={ref} className={cn('table__caption', className)} {...props} /> ), ); TableCaption.displayName = 'TableCaption';
Webflow
Pega en el Designer como application/json, luego convierte a Component (Atom / Table) y publica. Formato interno no documentado de Webflow — regenerable, no dependencia de runtime.
Setup del sitio (una vez)
Custom Code → Head:
<link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/tokens.css"> <link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/components.css">
Unsupported (no silencioso)
selectoron.table__footer .table__cell— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.table__head--right, .table__cell--right— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.table__head--center, .table__cell--center— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)@mediaon(prefers-reduced-motion: reduce)— not a Designer breakpoint — moved to head Custom Code block
Tras pegar: Create component → nombre Atom / Table → Publish.