Librerías / Table

Table

Biblioteca para visualizar datos en formato tabular.

@gravity-ui/table · npm package CI storybook

Instalación

npm install --save @gravity-ui/table

Uso

import React from 'react';
import {Table, useTable} from '@gravity-ui/table';
import type {ColumnDef} from '@gravity-ui/table/tanstack';

interface Person {
  id: string;
  name: string;
  age: number;
}

const columns: ColumnDef<Person>[] = [
  {accessorKey: 'name', header: 'Nombre', size: 100},
  {accessorKey: 'age', header: 'Edad', size: 100},
];

const data: Person[] = [
  {id: 'name', name: 'John', age: 23},
  {id: 'age', name: 'Michael', age: 27},
];

const BasicExample = () => {
  const table = useTable({
    columns,
    data,
  });

  return <Table table={table} />;
};

Componentes

Hay dos componentes Table que puedes usar:

  • BaseTable - un componente con estilos básicos únicamente;
  • Table - un componente con estilos basados en Gravity UI.

Selección de filas

import {selectionColumn} from '@gravity-ui/table';
import type {RowSelectionState} from '@gravity-ui/table/tanstack';

const columns: ColumnDef<Person>[] = [
  selectionColumn as ColumnDef<Person>,
  // ...otras columnas
];

const data: Person[] = [
  /* ... */
];

const RowSelectionExample = () => {
  const [rowSelection, setRowSelection] = React.useState<RowSelectionState>({});

  const table = useTable({
    columns,
    data,
    enableRowSelection: true,
    enableMultiRowSelection: true,
    onRowSelectionChange: setRowSelection,
    state: {
      rowSelection,
    },
  });

  return <Table table={table} />;
};

Para usar la agrupación con selección, utiliza el hook useRowSelectionFixedHandler. Sin él, el estado de la casilla de verificación de la fila principal será incorrecto. https://github.com/TanStack/table/issues/4878

Columna de selección de rango personalizada

El hook useToggleRangeSelectionHandler devuelve un manejador de cambios que escucha los eventos Shift+click y realiza la selección de filas por rango. Necesita que se le pase una instancia de CellContext para tener acceso a los estados internos de la tabla y de la fila.

import React, {type ChangeEvent, useCallback, useState} from 'react';

import {Table, useToggleRangeSelectionHandler, useTable} from '@gravity-ui/table';
import type {CellContext, ColumnDef, RowSelectionState} from '@gravity-ui/table/tanstack';
import {Checkbox, type CheckboxProps} from '@gravity-ui/uikit';

type CustomRangedSelectionCheckboxProps = Omit<CheckboxProps, 'onChange'> & {
  cellContext: CellContext<unknown, unknown>;
};

const CustomRangedSelectionCheckbox = ({
  className,
  cellContext,
  ...restProps
}: CustomRangedSelectionCheckboxProps) => {
  const rowToggleRangedSelectionHandler = useToggleRangeSelectionHandler(cellContext);

  const handleChange = useCallback(
    (event: ChangeEvent<HTMLInputElement>): void => {
      rowToggleRangedSelectionHandler(event);
    },
    [rowToggleRangedSelectionHandler],
  );

  return <Checkbox {...restProps} onChange={handleChange} />;
};

const customSelectionColumn: ColumnDef<unknown> = {
  id: '_select',
  header: ({table}) => (
    <Checkbox
      size="l"
      checked={table.getIsAllRowsSelected()}
      indeterminate={table.getIsSomeRowsSelected()}
      onChange={table.getToggleAllRowsSelectedHandler()}
    />
  ),
  cell: (cellContext) => (
    <CustomRangedSelectionCheckbox
      size="l"
      checked={cellContext.row.getIsSelected()}
      disabled={!cellContext.row.getCanSelect()}
      indeterminate={cellContext.row.getIsSomeSelected()}
      cellContext={cellContext}
    />
  ),
  size: 41,
  maxSize: 41,
  minSize: 41,
  enableResizing: false,
  enableSorting: false,
};

const columns: ColumnDef<Person>[] = [
  customSelectionColumn as ColumnDef<Person>,
  // ...otras columnas
];

const data: Person[] = [
  /* ... */
];

const RowRangedSelectionExample = () => {
  const [rowSelection, setRowSelection] = useState<RowSelectionState>({});

  const table = useTable({
    columns,
    data,
    enableRowSelection: true,
    enableMultiRowSelection: true,
    onRowSelectionChange: setRowSelection,
    state: {
      rowSelection,
    },
  });

  return <Table table={table} />;
};

También existe el componente RangedSelectionCheckbox, que utiliza el hook internamente y acepta una instancia de CellContext como prop. Este componente proporciona una forma abreviada de añadir funcionalidad de selección por rango a las columnas de selección personalizadas.

import type {ColumnDef} from '@gravity-ui/table/tanstack';
import {RangedSelectionCheckbox, SelectionCheckbox} from '@gravity-ui/table';

export const selectionColumn: ColumnDef<unknown> = {
  id: '_select',
  header: ({table}) => (
    <SelectionCheckbox
      checked={table.getIsAllRowsSelected()}
      disabled={!table.options.enableRowSelection}
      indeterminate={table.getIsSomeRowsSelected()}
      onChange={table.getToggleAllRowsSelectedHandler()}
    />
  ),
  cell: (cellContext) => (
    <RangedSelectionCheckbox
      checked={cellContext.row.getIsSelected()}
      disabled={!cellContext.row.getCanSelect()}
      indeterminate={cellContext.row.getIsSomeSelected()}
      cellContext={cellContext}
    />
  ),
  meta: {
    hideInSettings: true,
  },
  size: 32,
  minSize: 32,
};

Por defecto, la columna de selección generada con selectionColumn incluye la funcionalidad de selección por rango.

import {selectionColumn} from '@gravity-ui/table';
import type {ColumnDef} from '@gravity-ui/table/tanstack';

const columns: ColumnDef<Person>[] = [
  selectionColumn as ColumnDef<Person>,
  // ...otras columnas
];

Nota: Si la tabla contiene filas anidadas, la selección por rango no funcionará. En este momento, se considera un comportamiento indefinido.

Ordenación

Aprende sobre las propiedades de las columnas en la documentación de react-table aquí.

import type {SortingState} from '@gravity-ui/table/tanstack';

const columns: ColumnDef<Person>[] = [
  /* ... */
];

const data: Person[] = [
  /* ... */
];

const SortingExample = () => {
  const [sorting, setSorting] = React.useState<SortingState>([]);

  // Tu columna DEBE tener accessorFn para que la ordenación esté habilitada

  const table = useTable({
    columns,
    data,
    enableSorting: true,
    getRowId: (item) => item.id,
    onSortingChange: setSorting,
    state: {
      sorting,
    },
  });

  return <Table table={table} />;
};

Si quieres ordenar los elementos manualmente, pasa la propiedad manualSorting:

const table = useTable({
  // ...
  manualSorting: true,
});

Agrupación

import type {ExpandedState, Row} from '@gravity-ui/table/tanstack';

interface Person {
  id: string;
  name: string;
  age: number;
}

interface PersonGroup {
  id: string;
  name: string;
  items: Person[];
}

type Item = PersonGroup | Person;

const columns: ColumnDef<Item>[] = [
  {accessorKey: 'name', header: 'Nombre', size: 200},
  {accessorKey: 'age', header: 'Edad', size: 100},
];

const data: Item[] = [
  {
    id: 'friends',
    name: 'Amigos',
    items: [
      {id: 'nick', name: 'Nick', age: 25},
      {id: 'tom', name: 'Tom', age: 21},
    ],
  },
  {
    id: 'relatives',
    name: 'Familiares',
    items: [
      {id: 'john', name: 'John', age: 23},
      {id: 'michael', name: 'Michael', age: 27},
    ],
  },
];

const getGroupTitle = (row: Row<Item>) => row.getValue<string>('name');

const GroupingExample = () => {
  const [expanded, setExpanded] = React.useState<ExpandedState>({});

  const table = useTable({
    columns,
    data,
    enableExpanding: true,
    getSubRows: (item) => ('items' in item ? item.items : undefined),
    onExpandedChange: setExpanded,
    state: {
      expanded,
    },
  });

  return <Table table={table} getGroupTitle={getGroupTitle} />;
};

Para usar la agrupación con selección, utiliza el hook useRowSelectionFixedHandler. Sin él, el estado de la casilla de verificación de la fila principal será incorrecto. https://github.com/TanStack/table/issues/4878

Para habilitar los estilos de anidamiento, pasa withNestingStyles = true en la configuración de la columna.

Los indicadores de anidamiento se pueden deshabilitar pasando showTreeDepthIndicators = false.

Para añadir un control para expandir/colapsar filas, envuelve el contenido de la celda con el componente TreeExpandableCell o con tu propio componente similar:

import {TreeExpandableCell} from '@gravity-ui/table';

const columns: ColumnDef<Item>[] = [
  {
    accessorKey: 'name',
    header: 'Nombre',
    size: 200,
    showTreeDepthIndicators: false,
    withNestingStyles: true,
    cell: ({row, info}) => (
      <TreeExpandableCell row={row}>{info.getValue<string>()}</TreeExpandableCell>
    ),
  },
  // ...otras columnas
];

Reordenación

import type {ReorderingProviderProps} from '@gravity-ui/table';
import {dragHandleColumn, ReorderingProvider} from '@gravity-ui/table';

const columns: ColumnDef<Person>[] = [
  dragHandleColumn,
  // ...otras columnas
];

const data: Person[] = [
  /* ... */
];

const ReorderingExample = () => {
  const table = useTable({
    columns,
    data,
    getRowId: (item) => item.id,
  });

  const handleReorder = React.useCallback<
    NonNullable<ReorderingProviderProps<Person>['onReorder']>
  >(
    ({
      draggedItemKey,
      targetItemKey,
      baseItemKey,
      baseNextItemKey,
      enableNesting,
      nextChild,
      pullFromParent,
    }) => {
      // ...
    },
    [],
  );

  return (
    <ReorderingProvider table={table} onReorder={handleReorder}>
      <Table table={table} />
    </ReorderingProvider>
  );
};

Reordenación sin manejador de arrastre

Establece dragWithoutHandle para usar toda la fila como activador de arrastre y omite dragHandleColumn de las definiciones de columna:

const columns: ColumnDef<Person>[] = [
  {accessorKey: 'name', header: 'Nombre'},
  {accessorKey: 'age', header: 'Edad'},
];

return (
  <ReorderingProvider table={table} dragWithoutHandle onReorder={handleReorder}>
    <Table table={table} />
  </ReorderingProvider>
);

El puntero debe moverse 8 píxeles antes de que comience el arrastre, para que los clics normales en filas y controles sigan funcionando. Para excluir una parte personalizada de una fila de iniciar un arrastre, llama a preventDefault() en su manejador onPointerDown.

Reordenación de columnas

Envuelve la tabla con ColumnReorderingProvider para habilitar la reordenación de columnas mediante arrastrar y soltar en sus encabezados.

import {ColumnReorderingProvider} from '@gravity-ui/table';

const columns: ColumnDef<Person>[] = [
  {accessorKey: 'name', header: 'Nombre', size: 100},
  {accessorKey: 'age', header: 'Edad', size: 100},
];

const ColumnReorderingExample = () => {
  const table = useTable({
    columns,
    data,
    getRowId: (item) => item.id,
  });

  return (
    <ColumnReorderingProvider table={table}>
      <Table table={table} />
    </ColumnReorderingProvider>
  );
};

Reordenación de filas y columnas juntas

Anida ColumnReorderingProvider y ReorderingProvider para habilitar ambos ejes de arrastre a la vez. El orden de los proveedores no importa; comparten un único contexto dnd-kit internamente.

import type {ColumnReorderingProviderProps, ReorderingProviderProps} from '@gravity-ui/table';
import {ColumnReorderingProvider, ReorderingProvider, dragHandleColumn} from '@gravity-ui/table';

const columns: ColumnDef<Person>[] = [
  dragHandleColumn,
  {accessorKey: 'name', header: 'Nombre'},
  {accessorKey: 'age', header: 'Edad'},
];

const CombinedReorderingExample = () => {
  const [data, setData] = React.useState(initialData);
  const [columnOrder, setColumnOrder] = React.useState<string[]>([]);
  const table = useTable({
    columns,
    data,
    getRowId: (item) => item.id,
    state: {columnOrder},
    onColumnOrderChange: setColumnOrder,
  });

  const handleRowReorder = React.useCallback<
    NonNullable<ReorderingProviderProps<Person>['onReorder']>
  >(({draggedItemKey, baseItemKey}) => {
    // actualiza el array de datos
  }, []);

  const handleColumnReorder = React.useCallback<
    NonNullable<ColumnReorderingProviderProps<Person>['onReorder']>
  >(({columnOrder}) => {
    setColumnOrder(columnOrder);
  }, []);

  return (
    <ColumnReorderingProvider table={table} onReorder={handleColumnReorder}>
      <ReorderingProvider table={table} onReorder={handleRowReorder}>
        <Table table={table} />
      </ReorderingProvider>
    </ColumnReorderingProvider>
  );
};

Si controlas columnOrder tú mismo (por ejemplo, para persistirlo), pasa onReorder y aplica el orden resultante:

const [columnOrder, setColumnOrder] = React.useState<string[]>([]);

const table = useTable({
  columns,
  data,
  state: {columnOrder},
  onColumnOrderChange: setColumnOrder,
});

return (
  <ColumnReorderingProvider
    table={table}
    onReorder={({columnOrder}) => setColumnOrder(columnOrder)}
  >
    <Table table={table} />
  </ColumnReorderingProvider>
);

API de CSS:

Variable CSSValor predeterminadoDescripción
--gt-table-reordering-insertion-line-color#4d8bffColor de la línea de inserción de arrastre
--gt-table-reordering-insertion-line-width2pxAncho de la línea de inserción de arrastre
--gt-table-reordering-dragged-opacity0.4Opacidad de la columna arrastrada
--gt-table-drag-overlay-background#fffFondo de la vista previa de arrastre
--gt-table-drag-overlay-shadow0 3px 12px rgba(0,0,0,0.15)Sombra de la vista previa de arrastre
--gt-table-drag-overlay-border-radius6pxRadio del borde de la vista previa de arrastre

Para prohibir la reordenación de una columna específica, establece enableColumnReordering: false en su definición de columna. Las columnas de marcador de posición (agrupadas) no se pueden arrastrar. Usa activationDistance (predeterminado 8) para ajustar cuánto debe moverse el puntero antes de que comience un arrastre, lo que mantiene funcionando los clics en las cabeceras (como la ordenación).

Las columnas fijadas también se pueden reordenar, pero solo entre sí: una columna se puede mover dentro del grupo de fijación izquierda, el grupo de fijación derecha o el grupo central (sin fijar); nunca cruza un límite de fijación al arrastrar.

<ColumnReorderingProvider
  table={table}
  onReorder={({columnOrder, columnPinning, pinned}) => {
    if (pinned) {
      setColumnPinning(columnPinning);
    } else {
      setColumnOrder(columnOrder);
    }
  }}
>
  <Table table={table} />
</ColumnReorderingProvider>

Mientras arrastras:

  • una vista previa flotante de la columna (su cabecera más las primeras filas) sigue al puntero en una superposición de arrastre;
  • la columna arrastrada se vuelve semitransparente;
  • se dibuja una línea de inserción azul donde se soltará la columna;
<ColumnReorderingProvider
  table={table}
  autoScroll
  dragOverlayRowCount={10}
  renderDragOverlay={({columnId}) => <CustomColumnPreview columnId={columnId} />}
>
  <Table table={table} />
</ColumnReorderingProvider>

Virtualización

Úsalo si quieres usar el contenedor de la cuadrícula como elemento de desplazamiento (si quieres usar la ventana, consulta la sección de virtualización de ventana). Asegúrate de establecer una altura fija en el contenedor; de lo contrario, la virtualización no funcionará.

import {useRowVirtualizer} from '@gravity-ui/table';

const columns: ColumnDef<Person>[] = [
  /* ... */
];

const data: Person[] = [
  /* ... */
];

const VirtualizationExample = () => {
  const table = useTable({
    columns,
    data,
    getRowId: (item) => item.id,
  });

  const containerRef = React.useRef<HTMLDivElement>(null);

  const rowVirtualizer = useRowVirtualizer({
    count: table.getRowModel().rows.length,
    estimateSize: () => 20,
    overscan: 5,
    getScrollElement: () => containerRef.current,
  });

  return (
    <div ref={containerRef} style={{height: '500px', overflow: 'auto'}}>
      <Table table={table} rowVirtualizer={rowVirtualizer} />
    </div>
  );
};

Si usas virtualización con la función de reordenación, también necesitas pasar la opción rangeExtractor:

import {getVirtualRowRangeExtractor} from '@gravity-ui/table';

// ...

const tableRef = React.useRef<HTMLTableElement>(null);

const rowVirtualizer = useRowVirtualizer({
  // ...
  rangeExtractor: getVirtualRowRangeExtractor(tableRef.current),
});

return (
  <TableWithReordering
    ref={tableRef}
    table={table}
    rowVirtualizer={rowVirtualizer}
    onReorder={handleReorder}
  />
);

Virtualización de ventana

Úsalo si quieres usar la ventana como elemento de desplazamiento

import {useWindowRowVirtualizer} from '@gravity-ui/table';

const columns: ColumnDef<Person>[] = [
  /* ... */
];

const data: Person[] = [
  /* ... */
];

const WindowVirtualizationExample = () => {
  const table = useTable({
    columns,
    data,
    getRowId: (item) => item.id,
  });

  const bodyRef = React.useRef<HTMLTableSectionElement>(null);

  const rowVirtualizer = useWindowRowVirtualizer({
    count: table.getRowModel().rows.length,
    estimateSize: () => 20,
    overscan: 5,
    scrollMargin: bodyRef.current?.offsetTop ?? 0,
  });

  return <Table table={table} rowVirtualizer={rowVirtualizer} bodyRef={bodyRef} />;
};

Redimensionamiento

const columns: ColumnDef<Person>[] = [
  /* ... */
];
const data: Person[] = [
  /* ... */
];

const ResizingDemo = () => {
  const table = useTable({
    columns,
    data,
    enableColumnResizing: true,
    columnResizeMode: 'onChange',
  });

  return <Table table={table} />;
};

Configuración de columnas

const columns: ColumnDef<Person>[] = [
  // ...otras columnas
  {
    id: 'settings_column_id',
    header: ({table}) => <TableSettings table={table} />,
    meta: {
      hideInSettings: false, // Opcional. Permite ocultar esta columna del popover de configuración
      titleInSettings: 'ReactNode', // Opcional. Sobrescribe el campo header para el popover de configuración (si necesitas contenido diferente para el header y el popover de configuración)
    },
  }, // o puedes usar la función getSettingsColumn
];

const data: Person[] = [
  /* ... */
];

const TableSettingsDemo = () => {
  const [columnVisibility, onColumnVisibilityChange] = React.useState<VisibilityState>({
    // para control externo y estado inicial
    column_id: false, // para ocultar por defecto
  });
  const [columnOrder, onColumnOrderChange] = React.useState<string[]>([
    /* ids de columnas hoja */
  ]); // para control externo y estado inicial

  // Variante alternativa para obtener el estado, callbacks y establecer callbacks de aplicación de configuración - usando el hook useTableSettings:
  // const {state, callbacks} = useTableSettings({initialVisibility: {}, initialOrder: []})

  const table = useTable({
    columns,
    data,
    state: {
      columnVisibility,
      columnOrder,
    },
    onColumnVisibilityChange,
    onColumnOrderChange,
  });

  return <Table table={table} />;
};

Obtén más información sobre las propiedades de redimensionamiento de tablas y columnas en la documentación de react-table.

Problemas conocidos y compatibilidad

Compatibilidad con React 19 + React Compiler

⚠️ Problema conocido: Existe un problema de compatibilidad conocido con React 19 y React Compiler al usar @gravity-ui/table (que se basa en TanStack Table). La tabla puede no volver a renderizarse cuando los datos cambian. Consulta el problema #5567 de TanStack Table para obtener más detalles.

Solución:

Si estás utilizando React 19 con React Compiler y experimentas problemas con la re-renderización de la tabla, puedes usar la directiva 'use no memo' en el código de tu componente:

import React from 'react';
import {Table, useTable} from '@gravity-ui/table';
import type {ColumnDef} from '@gravity-ui/table/tanstack';

function MyTable() {
  'use no memo'; // Deshabilita la memoización de React Compiler para este componente

  const [data, setData] = React.useState<Person[]>([]);

  const table = useTable({
    data,
    columns,
  });

  return <Table table={table} />;
}

Solución alternativa:

También puedes memoizar explícitamente la instancia de la tabla o los datos para garantizar re-renderizaciones adecuadas:

import React from 'react';
import {Table, useTable} from '@gravity-ui/table';
import type {ColumnDef} from '@gravity-ui/table/tanstack';

function MyTable() {
  const [data, setData] = React.useState<Person[]>([]);

  // Memoiza explícitamente los datos para garantizar re-renderizaciones
  const memoizedData = React.useMemo(() => data, [data]);

  const table = useTable({
    data: memoizedData,
    columns,
  });

  return <Table table={table} />;
}

Nota: Este problema está en la biblioteca subyacente TanStack Table y deberá solucionarse allí. Las soluciones anteriores deberían ayudar hasta que haya una corrección disponible.

Licencia

Distribuido bajo la Licencia MIT. Consulta LICENSE para obtener más detalles.

Para agentes de IA

Una cuadrícula de datos sin encabezado, impulsada por TanStack-Table, para aplicaciones Gravity UI: utilízala para tablas ordenables, seleccionables, agrupables, reordenables y virtualizadas en lugar de componer marcado sin procesar sobre la tabla básica de uikit.

Cuándo usar

  • Grandes conjuntos de datos que necesitan virtualización de filas o ventanas (useRowVirtualizer, useWindowRowVirtualizer).
  • Ordenación, redimensionamiento, reordenación (ColumnReorderingProvider), fijación de columnas y configuración de columnas por usuario (TableSettings).
  • Selección de filas (individual/múltiple, por rango) y filas de árbol/agrupadas con celdas expandibles.

Cuándo no usar

  • Una tabla simple y estática con unas pocas filas y sin características avanzadas: la tabla integrada de uikit de @gravity-ui/uikit es más ligera.
  • Una lista no tabular: usa List de @gravity-ui/uikit.
  • Edición de celdas en línea estilo hoja de cálculo: esta cuadrícula está enfocada en lectura/visualización, no es una hoja de cálculo editable.

Errores comunes

  • Construyes la tabla con useTable, luego renderizas <Table table={table} />. La prop principal es table (la instancia), no data/columns directamente en <Table>; pasa data y columns a useTable.
  • Los tipos provienen de la subruta @gravity-ui/table/tanstack. Importa ColumnDef, RowSelectionState, SortingState, etc. de @gravity-ui/table/tanstack, no de la raíz del paquete.
  • La ordenación necesita un accesor. Una columna debe tener accessorKey/accessorFn para que la ordenación funcione; establece enableSorting y proporciona getRowId.
  • React 19 + React Compiler puede omitir re-renderizaciones. Este es un problema de TanStack Table de nivel superior: agrega la directiva 'use no memo' al componente o memoiza data.
  • La selección por rango falla con filas anidadas. El comportamiento de selección por rango no está definido cuando la tabla tiene filas agrupadas/anidadas; usa useRowSelectionFixedHandler para un estado de casilla de verificación principal correcto con la agrupación.

Documentación para agentes de IA

La documentación legible por agentes para la versión instalada se encuentra en node_modules/@gravity-ui/table/build/docs/INDEX.md.

Acerca de la librería
Apoya la librería con una estrella
Versión
1.20.1
Última actualización
07.08.2026
Repositorio
github.com/gravity-ui/table
Licencia
MIT License
Mantenedores
Colaboradores