Data Table

1.2.5

A **Data Table** é um padrão que organiza informações estruturadas em linhas e colunas, facilitando sua exploração, comparação e manipulação. É utilizada para listar grandes volumes de dados (como produtos, pedidos ou clientes), permitindo realizar ações de forma eficiente e precisa a partir da mesma vista.

import React, { useEffect, useState } from "react";
import { DataTable } from "@nimbus-ds/patterns";
import { Tag, Box, IconButton, Chip } from "@nimbus-ds/components";
import {
  ChevronDownIcon,
  CheckCircleIcon,
  ChevronUpIcon,
  ExclamationTriangleIcon,
} from "@nimbus-ds/icons";


const pageSize = 5;

const orders = [
  {
    id: 10,
    clientName: "Dr. Johnnie Bins",
    total: "R$16.788,20",
    qty: "9",
    status: false,
  },
  {
    id: 9,
    clientName: "Earnest Berge",
    total: "R$62.657,83",
    qty: "3",
    status: false,
  },
  {
    id: 8,
    clientName: "Irene Purdy",
    total: "R$17.692,10",
    qty: "4",
    status: false,
  },
  {
    id: 7,
    clientName: "Owen Swift DVM",
    total: "R$60.269,67",
    qty: "5",
    status: false,
  },
  {
    id: 6,
    clientName: "Felipe Ferry",
    total: "R$75.058,94",
    qty: "3",
    status: false,
  },
  {
    id: 5,
    clientName: "Derek Kub",
    total: "R$29.068,91",
    qty: "1",
    status: false,
  },
  {
    id: 4,
    clientName: "Elisa Vandervort",
    total: "R$22.636,41",
    qty: "5",
    status: false,
  },
  {
    id: 3,
    clientName: "Rochelle Spencer",
    total: "R$76.244,05",
    qty: "9",
    status: false,
  },
  {
    id: 2,
    clientName: "Angelina Koelpin",
    total: "R$65.306,79",
    qty: "4",
    status: false,
  },
  {
    id: 1,
    clientName: "Edna Jacobi",
    total: "R$97.025,32",
    qty: "6",
    status: false,
  },
];

const Example: React.FC = () => {
  interface RowProps {
    id: number;
    clientName: string;
    total: string;
    qty: string;
    status: boolean;
  }

  const [rows, setRows] = useState<RowProps[]>(orders);
  const [checkedRows, setCheckedRows] = useState<number[]>([]);
  const [headerCheckboxStatus, setHeaderCheckboxStatus] = useState(false);
  const [headerIndeterminateStatus, setHeaderIndeterminateStatus] =
    useState(false);
  const [currentPage, setCurrentPage] = useState<number>(1);
  const [sortDirection, setSortDirection] = useState<
    "ascending" | "descending"
  >("descending");
  const [sortColumn, setSortColumn] = useState<"id" | "clientName">("id");

  useEffect(() => {
    if (checkedRows.length === rows.length) {
      setHeaderCheckboxStatus(true);
      setHeaderIndeterminateStatus(false);
    } else if (checkedRows.length > 0) {
      setHeaderCheckboxStatus(false);
      setHeaderIndeterminateStatus(true);
    } else {
      setHeaderCheckboxStatus(false);
      setHeaderIndeterminateStatus(false);
    }
  }, [checkedRows.length, rows.length]);

  const handleRowClick = (id: number) => {
    if (checkedRows.includes(id)) {
      setCheckedRows(checkedRows.filter((rowId) => rowId !== id));
    } else {
      setCheckedRows([...checkedRows, id]);
    }
  };

  const handleHeaderCheckboxClick = () => {
    if (headerCheckboxStatus) {
      setCheckedRows([]);
    } else {
      const rowIds = rows.map((row) => row.id);
      setCheckedRows(rowIds);
    }
  };

  const handleBulkUpdateStatusClick = (status: boolean) => {
    const updatedRows = rows.map((row) => {
      const checked = checkedRows.includes(row.id);
      return { ...row, status: checked ? status : row.status };
    });
    setRows(updatedRows);
  };

  const handlePageChange = (page: number): void => {
    setCurrentPage(page);
  };

  const handleSort = (column: "id" | "clientName") => {
    if (column === sortColumn) {
      setSortDirection(
        sortDirection === "ascending" ? "descending" : "ascending"
      );
    } else {
      setSortColumn(column);
      setSortDirection("ascending");
    }
  };

  const sortCompareFunction = (rowA: RowProps, rowB: RowProps) => {
    if (sortColumn === "id") {
      return sortDirection === "ascending"
        ? rowA.id - rowB.id
        : rowB.id - rowA.id;
    }
    if (sortColumn === "clientName") {
      return sortDirection === "ascending"
        ? rowA.clientName.localeCompare(rowB.clientName)
        : rowB.clientName.localeCompare(rowA.clientName);
    }
    return 0;
  };

  const getDisplayedRows = (): RowProps[] => {
    const sortedRows = rows.slice().sort(sortCompareFunction);
    const startIndex = (currentPage - 1) * pageSize;
    const endIndex = startIndex + pageSize;
    return sortedRows.slice(startIndex, endIndex);
  };

  const displayedRows = getDisplayedRows();
  const totalRows = rows.length;
  const firstRow = (currentPage - 1) * pageSize + 1;
  const lastRow = Math.min(currentPage * pageSize, totalRows);

  const tableHeader = (
    <DataTable.Header
      checkbox={{
        name: "check-all-rows",
        checked: headerCheckboxStatus,
        onChange: handleHeaderCheckboxClick,
        indeterminate: headerIndeterminateStatus,
      }}
    >
      <DataTable.Cell width="120px">
        <Box display="flex" gap="2" alignItems="center">
          Order no.
          <IconButton
            source={
              sortDirection === "ascending" ? (
                <ChevronUpIcon size={10} />
              ) : (
                <ChevronDownIcon size={10} />
              )
            }
            size="1rem"
            onClick={() => handleSort("id")}
          />
        </Box>
      </DataTable.Cell>
      <DataTable.Cell width="auto">Client name</DataTable.Cell>
      <DataTable.Cell width="120px">Total</DataTable.Cell>
      <DataTable.Cell width="120px">Qty. of products</DataTable.Cell>
      <DataTable.Cell width="120px">Order status</DataTable.Cell>
    </DataTable.Header>
  );

  const tableFooter = (
    <DataTable.Footer
      itemCount={`Showing ${firstRow}-${lastRow} orders of ${totalRows}`}
      pagination={{
        pageCount: Math.ceil(totalRows / pageSize),
        activePage: currentPage,
        onPageChange: handlePageChange,
      }}
    />
  );

  const hasBulkActions = checkedRows.length > 0 && (
    <DataTable.BulkActions
      checkbox={{
        name: "check-all",
        checked: headerCheckboxStatus,
        onChange: handleHeaderCheckboxClick,
        indeterminate: headerIndeterminateStatus,
      }}
      label={`${checkedRows.length} selected`}
      action={
        <Box display="flex" gap="1">
          <Chip
            onClick={() => handleBulkUpdateStatusClick(true)}
            text="Fulfill orders"
          />
          <Chip
            onClick={() => handleBulkUpdateStatusClick(false)}
            text="Unfulfill orders"
          />
        </Box>
      }
    />
  );

  return (
    <DataTable
      header={tableHeader}
      footer={tableFooter}
      bulkActions={hasBulkActions}
    >
      {displayedRows.map((row) => {
        const { id, status } = row;
        const statusIcon = status ? (
          <CheckCircleIcon />
        ) : (
          <ExclamationTriangleIcon />
        );
        const statusAppearance = status ? "success" : "warning";
        const statusMsg = status ? "Fulfilled" : "Pending";

        return (
          <DataTable.Row
            key={id}
            backgroundColor={
              checkedRows.includes(id)
                ? {
                    rest: "primary-surface",
                    hover: "primary-surfaceHighlight",
                  }
                : {
                    rest: "neutral-background",
                    hover: "neutral-surface",
                  }
            }
            checkbox={{
              name: `check-${id}`,
              checked: checkedRows.includes(id),
              onChange: () => handleRowClick(id),
            }}
          >
            <DataTable.Cell>#{row.id}</DataTable.Cell>
            <DataTable.Cell>{row.clientName}</DataTable.Cell>
            <DataTable.Cell>{row.total}</DataTable.Cell>
            <DataTable.Cell>{row.qty}</DataTable.Cell>
            <DataTable.Cell>
              <Tag appearance={statusAppearance}>
                {statusIcon}
                {statusMsg}
              </Tag>
            </DataTable.Cell>
          </DataTable.Row>
        );
      })}
    </DataTable>
  );
};

export default Example;

Instale o componente via terminal.

npm install @nimbus-ds/data-table

Data Table

  • Para mostrar listas extensas de objetos com múltiplos atributos comparáveis (produtos, pedidos, usuários, etc.).
  • Quando se requer ordenamento, filtrado, busca ou edição em linha de dados.
  • Ideal em fluxos de gestão intensiva onde se espera trabalhar com muitos itens de uma vez.
  • Quando os dados são muito poucos (menos de 3 itens) ou não têm atributos comparáveis.
  • Se os itens requerem visualizações enriquecidas ou elementos visuais destacados (usar cards sempre que sejam poucos elementos para listar).
  • Quando se busca uma experiência mais conversacional ou de assistência (usar wizard ou formulários sequenciais).
  • Ordenável e filtrável: O usuário pode encontrar rapidamente o que busca.
  • Suporte para ações em lote: Agiliza tarefas repetitivas como eliminar, mudar estado, exportar.
  • Edição inline quando aplicável: Minimiza a mudança de interface e melhora a velocidade de edição.
  • Persistência de filtros e estado entre sessões.
  • Suporte para shortcuts e navegação com teclado.
  • Feedback imediato ante ações (toast)
  • Navegação completa com teclado: navegação por células, uso de Tab/Shift+Tab.
  • Roles semânticos corretos: role="table", role="row", role="cell", aria-sort, etc.
  • Alternativas de texto e estados acessíveis para leitores de tela.
  • Foco visível e consistente ao interagir com elementos dentro da tabela.
  • Áreas táteis adequadas em dispositivos móveis (mínimo 44x44px).

A estrutura de uma Data Table inclui:

  1. Header: Com nomes de colunas e controles de ordenamento.
  2. Rows: Cada linha representa um item.
  3. Cells: Contêm os valores ou ações.
  4. Bulk actions bar (opcional): Aparece ao selecionar itens.
  5. Paginação ou scroll infinito.
  6. Toolbar (opcional): Filtros, busca e botões de ação geral.
  • Checkbox
  • Icon button
  • Tag
  • Tooltip
  • Search field
  • Pagination
  • Text

Do

  • Usar ordenamento e filtros consistentes com o modelo mental do usuário.
  • Manter os headers sempre visíveis ao fazer scroll.
  • Priorizar colunas chave à esquerda.
  • Oferecer feedback imediato ao editar (usar toast)
  • Limitar o número de colunas visíveis em mobile.

Don't

  • Não incluir muitas colunas sem permitir scroll ou colapso.
  • Não usar ações sem ícones ou textos descritivos.
  • Não modificar o layout ao aplicar filtros (preservar contexto).
  • Não apresentar dados sem alinhamento coerente (texto, números, datas).

Additional props are passed to the <DataTable> element. See div docs for a list of props accepted by the <DataTable> element.

DataTable

NameTypeDefaultDescription

bulkActions

React.ReactNode

Bulk actions component rendered with a sticky position over the top of the table element.

header*

React.ReactNode

Table header content.

footer

React.ReactNode

Optional table footer content.

children*

React.ReactNode

Table body content.

DataTable.BulkActions

NameTypeDefaultDescription

checkbox*

object

Properties of the checkbox element rendered in the Bulk Actions component.

link

<Link />

Optional link element rendered next to the Bulk Actions controller.

action*

React.ReactNode

Action component that controls the Bulk Actions.

label*

string

Lable for the checkbox element.

DataTable.Header

NameTypeDefaultDescription

checkbox*

object

Checkbox element rendered on the table header that controls all rows.

children*

React.ReactNode

Row content.

DataTable.Footer

NameTypeDefaultDescription

itemCount*

string

Left-hand side text intended for displaying an item count.

pagination

object

Pagination element rendered on the right-side of the footer.

DataTable.Row

NameTypeDefaultDescription

checkbox*

object

Checkbox element rendered on the row that controls whether the row is selected.

children*

React.ReactNode

Content of the row.

DataTable.Cell

NameTypeDefaultDescription

children*

React.ReactNode

Content of the List component.