Ir al contenido
Evaluar mi proyecto
Componentes · Datos y visualizaciónDisponible

Tabla ordenable de registros

Tabla semántica con búsqueda, orden por nombre o importe, estado vacío y desplazamiento horizontal en móvil.

Proyectos de muestra · importes en USD
Estado
Auditoría de interfazCompletadoUSD 900
Portal de clientesPendienteUSD 4,600
Sistema de diseñoEn cursoUSD 3,200
Sitio editorialCompletadoUSD 1,800

4 de 4 registros

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

datos-tabla.tsx182 líneas
Descargar
"use client";

import { useState, useId, type CSSProperties } from "react";

/** Tabla ordenable de registros. React + TypeScript, sin dependencias. Personaliza con variables --gczt-*. */
export type TableRecord = {
  id: string;
  name: string;
  status: "En curso" | "Completado" | "Pendiente";
  amount: number;
};
export type DatosTablaProps = {
  rows?: TableRecord[];
  currency?: string;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gczt-root { box-sizing: border-box; min-width: 0; max-width: 100%; color: var(--gczt-fg, #202421); font: 400 15px/1.5 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.gczt-root *, .gczt-root *::before, .gczt-root *::after { box-sizing: border-box; }
.gczt-root button, .gczt-root input, .gczt-root textarea, .gczt-root select { font: inherit; }
.gczt-root button, .gczt-root a { -webkit-tap-highlight-color: transparent; }
.gczt-root :focus-visible { outline: 2px solid var(--gczt-accent, #25724b); outline-offset: 4px; }
.gczt-root ::selection { background: var(--gczt-accent, #25724b); color: #fff; }
.gczt-root button:disabled { opacity: .5; cursor: not-allowed; }
.gczt-root svg { flex-shrink: 0; }
@media (prefers-reduced-motion: reduce) { .gczt-root *, .gczt-root *::before, .gczt-root *::after { animation: none !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } }
.gczt-root { width:700px; padding:24px; border-radius:12px; background:var(--gczt-bg,#fff); }
.gczt-toolbar { display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap; margin-bottom:20px; }
.gczt-toolbar label { font-size:14px; font-weight:600; }
.gczt-toolbar input { min-height:44px; min-width:0; max-width:100%; border:1px solid #c7d0db; border-radius:5px; padding:10px 12px; background:#f9fafc; color:#304357; font-size:12px; }
.gczt-toolbar input::placeholder { color:#637286; }
.gczt-scroll { overflow-x:auto; scrollbar-color:#b2bfce #eef2f6; scrollbar-width:thin; }
.gczt-root table { width:100%; border-collapse:collapse; text-align:left; font-size:12px; min-width:440px; }
.gczt-root caption { text-align:left; caption-side:bottom; padding-top:15px; font-size:10px; color:#637286; }
.gczt-root th,.gczt-root td { padding:14px 10px; border-bottom:1px solid #e2e7ed; font-weight:400; white-space:nowrap; }
.gczt-root thead th { color:#586a80; background:#f1f4f8; font-weight:500; }
.gczt-root th button { border:0; padding:0; min-height:30px; background:transparent; color:inherit; cursor:pointer; font-size:12px; display:flex; gap:12px; align-items:center; }
.gczt-root tbody th { color:#304357; font-weight:550; }
.gczt-root td:last-child { font-variant-numeric:tabular-nums; }
.gczt-root [data-status] { display:inline-block; padding:4px 8px; border-radius:4px; background:#f4eedc; color:#795b22; font-size:10px; }
.gczt-root [data-status="Completado"] { background:#e7f0e6; color:#315c39; }
.gczt-root [data-status="En curso"] { background:#e4ecf7; color:#36577e; }
.gczt-root>p { font-size:11px; color:#637286; margin:16px 0 0; }`;

export function DatosTabla({
  rows = [
    { id: "001", name: "Sistema de diseño", status: "En curso", amount: 3200 },
    { id: "002", name: "Sitio editorial", status: "Completado", amount: 1800 },
    {
      id: "003",
      name: "Portal de clientes",
      status: "Pendiente",
      amount: 4600,
    },
    {
      id: "004",
      name: "Auditoría de interfaz",
      status: "Completado",
      amount: 900,
    },
  ],
  currency = "USD",
  className = "",
  style,
}: DatosTablaProps) {
  const [query, setQuery] = useState("");
  const [sort, setSort] = useState<{
    key: "name" | "amount";
    descending: boolean;
  }>({ key: "name", descending: false });
  const id = useId();
  const fmt = new Intl.NumberFormat("es-MX", {
    style: "currency",
    currency,
    maximumFractionDigits: 0,
  });
  const visible = rows
    .filter((row) =>
      row.name.toLocaleLowerCase().includes(query.toLocaleLowerCase()),
    )
    .sort(
      (a, b) =>
        (sort.key === "name"
          ? a.name.localeCompare(b.name)
          : a.amount - b.amount) * (sort.descending ? -1 : 1),
    );
  function order(key: "name" | "amount") {
    setSort({ key, descending: sort.key === key ? !sort.descending : false });
  }
  return (
    <div className={`gczt-root ${className}`} style={style}>
      <style>{CSS}</style>
      <div className="gczt-toolbar">
        <label htmlFor={id}>Buscar registros</label>
        <input
          id={id}
          type="search"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Nombre del proyecto"
        />
      </div>
      <div
        className="gczt-scroll"
        tabIndex={0}
        role="region"
        aria-label="Tabla desplazable"
      >
        <table>
          <caption>Proyectos de muestra · importes en {currency}</caption>
          <thead>
            <tr>
              <th
                scope="col"
                aria-sort={
                  sort.key === "name"
                    ? sort.descending
                      ? "descending"
                      : "ascending"
                    : "none"
                }
              >
                <button type="button" onClick={() => order("name")}>
                  Proyecto{" "}
                  <span aria-hidden="true">
                    {sort.key === "name" ? (sort.descending ? "↓" : "↑") : "↕"}
                  </span>
                </button>
              </th>
              <th scope="col">Estado</th>
              <th
                scope="col"
                aria-sort={
                  sort.key === "amount"
                    ? sort.descending
                      ? "descending"
                      : "ascending"
                    : "none"
                }
              >
                <button type="button" onClick={() => order("amount")}>
                  Importe{" "}
                  <span aria-hidden="true">
                    {sort.key === "amount"
                      ? sort.descending
                        ? "↓"
                        : "↑"
                      : "↕"}
                  </span>
                </button>
              </th>
            </tr>
          </thead>
          <tbody>
            {visible.map((row) => (
              <tr key={row.id}>
                <th scope="row">{row.name}</th>
                <td>
                  <span data-status={row.status}>{row.status}</span>
                </td>
                <td>{fmt.format(row.amount)}</td>
              </tr>
            ))}
            {!visible.length && (
              <tr>
                <td colSpan={3}>
                  No hay registros que coincidan. Prueba otra búsqueda.
                </td>
              </tr>
            )}
          </tbody>
        </table>
      </div>
      <p role="status">
        {visible.length} de {rows.length} registros
      </p>
    </div>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/datos-tabla

Ejemplo de uso de Tabla ordenable de registros

Después de copiar datos-tabla.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { DatosTabla } from "./datos-tabla";

export default function Ejemplo() {
  return <DatosTabla currency="Ejemplo" />;
}

Propiedades de DatosTabla

DatosTabla acepta 4 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
rows?TableRecord[]Opcional.
currency?stringOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Personaliza Tabla ordenable de registros con variables CSS

Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.

VariableValor por defecto
--gczt-fg#202421
--gczt-accent#25724b
--gczt-bg#fff

Accesibilidad y rendimiento

El archivo tiene 182 líneas (7 KB) y no necesita @keyframes. Esto es lo que hace Tabla ordenable de registros por quien lo usa:

  • Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
  • Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
  • Declara roles ARIA para lectores de pantalla: region, status.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)