# Table / Tabla

> Table with column sorting and horizontal scroll on mobile.

Page: https://componentes-lovat.vercel.app/c/table/  
License: MIT · Design: coss ui (MIT) · table.tsx  
Requires: tokens.css, badge  
Context: herramienta · Density: alta · Tone: sobria

## Files

- Tokens (once): https://componentes-lovat.vercel.app/tokens.css
- Dependency `badge`: https://componentes-lovat.vercel.app/r/badge.json
- HTML: https://componentes-lovat.vercel.app/raw/table/table.html
- CSS: https://componentes-lovat.vercel.app/raw/table/table.css
- JS: https://componentes-lovat.vercel.app/raw/table/table.js
- Everything in one file: https://componentes-lovat.vercel.app/r/table.html

Dark theme: add class `dark` to `<html>`.

## When to use

- For data compared by rows and columns.

## When not to use

- For layout: use grid.

## Accessibility

Roles: table, columnheader, button

| Keys | Action |
|---|---|
| Tab | Reaches the sort buttons |
| Enter / Espacio | Sorts the column |

- To sort, a button inside the header and aria-sort on the column.
- The wrapper scrolls with the keyboard (tabindex=0 and a name).

## Code

```html
<div class="c-table-wrap" role="region" aria-labelledby="tbl-caption" tabindex="0">
  <table class="c-table" data-c-sortable>
    <caption class="c-table__caption c-sr-only" id="tbl-caption">Facturas de septiembre 2026</caption>
    <thead>
      <tr>
        <th class="c-table__head" scope="col" aria-sort="descending"><button class="c-table__sort" type="button" data-key="0">Factura <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m18 15-6-6-6 6"/></svg></button></th>
        <th class="c-table__head" scope="col"><button class="c-table__sort" type="button" data-key="1">Cliente <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m18 15-6-6-6 6"/></svg></button></th>
        <th class="c-table__head" scope="col">Estado</th>
        <th class="c-table__head c-table__head--num" scope="col"><button class="c-table__sort" type="button" data-key="3" data-type="num">Monto <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m18 15-6-6-6 6"/></svg></button></th>
      </tr>
    </thead>
    <tbody>
      <tr class="c-table__row"><td class="c-table__cell">INV-0914</td><td class="c-table__cell">Northwind</td><td class="c-table__cell"><span class="c-badge c-badge--success"><span class="c-badge__dot" aria-hidden="true"></span>Pagado</span></td><td class="c-table__cell c-table__cell--num" data-value="89">S/ 89.00</td></tr>
      <tr class="c-table__row"><td class="c-table__cell">INV-0913</td><td class="c-table__cell">Vela Labs</td><td class="c-table__cell"><span class="c-badge c-badge--success"><span class="c-badge__dot" aria-hidden="true"></span>Pagado</span></td><td class="c-table__cell c-table__cell--num" data-value="249">S/ 249.00</td></tr>
      <tr class="c-table__row"><td class="c-table__cell">INV-0912</td><td class="c-table__cell">Tallyworks</td><td class="c-table__cell"><span class="c-badge c-badge--warning"><span class="c-badge__dot" aria-hidden="true"></span>Pendiente</span></td><td class="c-table__cell c-table__cell--num" data-value="29">S/ 29.00</td></tr>
      <tr class="c-table__row"><td class="c-table__cell">INV-0911</td><td class="c-table__cell">Harbor &amp; Co</td><td class="c-table__cell"><span class="c-badge c-badge--error"><span class="c-badge__dot" aria-hidden="true"></span>Vencido</span></td><td class="c-table__cell c-table__cell--num" data-value="101">S/ 101.00</td></tr>
    </tbody>
  </table>
</div>
<p class="c-sr-only" role="status" data-c-table-status></p>
```

```css
/* table · Componentes (MIRÓ Labs). Diseño de coss ui (MIT, © coss.com, antes Origin UI).
   Generado por tools/build_css.py desde table.tw.json. No editar a mano. Necesita tokens.css. */
.c-table-wrap{position:relative}
.c-table__row{position:relative}
.c-table__caption{margin-top:calc(var(--spacing)*4)}
.c-table__head{height:calc(var(--spacing)*10)}
.c-table-wrap{width:100%}
.c-table{width:100%;caption-side:bottom}
.c-table-wrap{overflow-x:auto}
.c-table thead tr{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}
.c-table__row{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}
.c-table__cell{background-clip:padding-box;padding:calc(var(--spacing)*2.5)}
.c-table__head{padding-inline:calc(var(--spacing)*2.5);text-align:left;vertical-align:middle}
.c-table__cell{vertical-align:middle}
.c-table{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}
.c-table__caption{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}
.c-table__head{--tw-leading:1;line-height:1}
.c-table__cell{--tw-leading:1;line-height:1}
.c-table__head{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);white-space:nowrap}
.c-table__cell{white-space:nowrap}
.c-table__head{color:var(--muted-foreground)}
.c-table__caption{color:var(--muted-foreground)}
/* propio */
.c-table{border-collapse:collapse}
.c-table tbody tr:last-child{border-bottom:0}
.c-table__cell--num,.c-table__head--num{text-align:right;font-variant-numeric:tabular-nums}
.c-table__sort{display:inline-flex;align-items:center;gap:.25rem;margin:0 -.375rem;padding:.25rem .375rem;border:0;border-radius:calc(var(--radius) - 4px);background:transparent;font:inherit;color:inherit;cursor:pointer}
.c-table__sort:hover{color:var(--foreground);background:var(--accent)}
.c-table__sort:focus-visible{outline:2px solid var(--ring);outline-offset:1px}
.c-table__sort svg{width:.875rem;height:.875rem;opacity:.5}
[aria-sort=ascending] .c-table__sort svg,[aria-sort=descending] .c-table__sort svg{opacity:1}
[aria-sort=descending] .c-table__sort svg{rotate:180deg}
.c-table-wrap{border:1px solid var(--border);border-radius:calc(var(--radius) + 4px)}
.c-table__head:first-child,.c-table__cell:first-child{padding-left:.875rem}
.c-table__head:last-child,.c-table__cell:last-child{padding-right:.875rem}
.c-table tbody .c-table__row:hover{background-color:color-mix(in srgb,var(--background),var(--color-black) 2%)}
.dark .c-table tbody .c-table__row:hover{background-color:color-mix(in srgb,var(--background),var(--color-white) 2%)}
.c-sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
```

```js
/* Tabla ordenable: el botón del encabezado ordena y marca aria-sort (ascendente o descendente). */
(function () {
  const C = (window.Componentes = window.Componentes || {});
  function setup(table) {
    const body = table.tBodies[0];
    table.querySelectorAll('.c-table__sort').forEach((btn) => btn.addEventListener('click', () => {
      const th = btn.closest('th'), i = +btn.dataset.key, num = btn.dataset.type === 'num';
      const dir = th.getAttribute('aria-sort') === 'ascending' ? 'descending' : 'ascending';
      table.querySelectorAll('th[aria-sort]').forEach((h) => h.removeAttribute('aria-sort'));
      th.setAttribute('aria-sort', dir);
      const val = (tr) => { const td = tr.cells[i]; return num ? parseFloat(td.dataset.value ?? td.textContent) : td.textContent.trim(); };
      const rows = [...body.rows].sort((a, b) => { const x = val(a), y = val(b); const r = num ? x - y : String(x).localeCompare(String(y), 'es'); return dir === 'ascending' ? r : -r; });
      rows.forEach((r) => body.append(r));
      const st = document.querySelector('[data-c-table-status]');
      if (st) st.textContent = `Ordenado por ${btn.textContent.trim()}, ${dir === 'ascending' ? 'ascendente' : 'descendente'}.`;
    }));
  }
  function init(root) {
    (root || document).querySelectorAll('table[data-c-sortable]:not([data-c-ready])').forEach((t) => { t.dataset.cReady = ''; try { setup(t); } catch (e) { console.warn('[table]', e); } });
  }
  C.table = { init };
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => init()); else init();
})();
```

## Credits

- coss ui (MIT) · table.tsx: https://coss.com/ui
- Adrian Roselli, Sortable Table Columns: https://adrianroselli.com/2021/04/sortable-table-columns.html
