Uranus® Design System
Blocos

DataTable

Tabela de dados genérica baseada em TanStack Table com API composicional - sorting, paginação client-side, toolbar e pagination como children.

DataTable é a opinião da Uranus sobre tabelas de dados, construída sobre TanStack Table v8 e o primitivo Table do design system. A API é composicional: você combina DataTable.Provider, DataTable.Root, DataTable.Toolbar e DataTable.Pagination como children, e qualquer célula custom acessa a Table instance via useDataTable<T>().

Esta é a única dependência runtime que o @uranus-workspace/blocks adiciona em relação ao primitivo Table.

Clientes ativos

8 clientes neste workspace.

Clientes ativos no workspace
AC
Alice Costaalice@uranus.com.br
Enterprise
ativo
R$ 12.800,00
BL
Bruno Limabruno@uranus.com.br
Pro
ativo
R$ 870,00
CS
Camila Souzacamila@uranus.com.br
Free
convidado
R$ 0,00
DA
Diego Almeidadiego@uranus.com.br
Pro
pausado
R$ 99,00
EM
Erika Martinserika@uranus.com.br
Enterprise
ativo
R$ 19.900,00
Página 1 de 2
'use client';
import type { ColumnDef } from '@tanstack/react-table';
import { DataTable, useDataTable } from '@uranus-workspace/blocks';
import {
  Avatar,
  AvatarFallback,
  Badge,
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  Input,
} from '@uranus-workspace/design-system';
import { Search } from 'lucide-react';

interface Customer {
  id: string;
  name: string;
  email: string;
  plan: 'Free' | 'Pro' | 'Enterprise';
  status: 'ativo' | 'convidado' | 'pausado';
  amount: number;
}

const customers: Customer[] = [
  {
    id: '1',
    name: 'Alice Costa',
    email: 'alice@uranus.com.br',
    plan: 'Enterprise',
    status: 'ativo',
    amount: 12_800,
  },
  {
    id: '2',
    name: 'Bruno Lima',
    email: 'bruno@uranus.com.br',
    plan: 'Pro',
    status: 'ativo',
    amount: 870,
  },
  {
    id: '3',
    name: 'Camila Souza',
    email: 'camila@uranus.com.br',
    plan: 'Free',
    status: 'convidado',
    amount: 0,
  },
  {
    id: '4',
    name: 'Diego Almeida',
    email: 'diego@uranus.com.br',
    plan: 'Pro',
    status: 'pausado',
    amount: 99,
  },
  {
    id: '5',
    name: 'Erika Martins',
    email: 'erika@uranus.com.br',
    plan: 'Enterprise',
    status: 'ativo',
    amount: 19_900,
  },
  {
    id: '6',
    name: 'Felipe Andrade',
    email: 'felipe@uranus.com.br',
    plan: 'Pro',
    status: 'ativo',
    amount: 615,
  },
  {
    id: '7',
    name: 'Gabriela Tavares',
    email: 'gabriela@uranus.com.br',
    plan: 'Free',
    status: 'convidado',
    amount: 0,
  },
  {
    id: '8',
    name: 'Henrique Pires',
    email: 'henrique@uranus.com.br',
    plan: 'Pro',
    status: 'ativo',
    amount: 740,
  },
];

const currency = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' });

const statusVariant: Record<Customer['status'], 'default' | 'secondary' | 'outline'> = {
  ativo: 'default',
  convidado: 'secondary',
  pausado: 'outline',
};

const initialsOf = (name: string) =>
  name
    .split(' ')
    .map((part) => part[0])
    .filter(Boolean)
    .slice(0, 2)
    .join('')
    .toUpperCase();

const columns: ColumnDef<Customer, unknown>[] = [
  {
    accessorKey: 'name',
    header: 'Cliente',
    cell: ({ row }) => (
      <div className="flex items-center gap-3">
        <Avatar className="size-8">
          <AvatarFallback className="text-[11px] font-medium">
            {initialsOf(row.original.name)}
          </AvatarFallback>
        </Avatar>
        <div className="flex flex-col">
          <span className="font-medium text-foreground">{row.original.name}</span>
          <span className="text-xs text-muted-foreground">{row.original.email}</span>
        </div>
      </div>
    ),
  },
  {
    accessorKey: 'plan',
    header: 'Plano',
    cell: ({ row }) => <span className="text-sm">{row.original.plan}</span>,
  },
  {
    accessorKey: 'status',
    header: 'Status',
    cell: ({ row }) => (
      <Badge variant={statusVariant[row.original.status]} className="capitalize">
        {row.original.status}
      </Badge>
    ),
  },
  {
    accessorKey: 'amount',
    header: () => <div className="text-right">MRR</div>,
    cell: ({ row }) => (
      <div className="text-right font-mono text-sm tabular-nums">
        {currency.format(row.original.amount)}
      </div>
    ),
  },
];

function NameSearch() {
  const { table } = useDataTable<Customer>();
  const value = (table.getColumn('name')?.getFilterValue() as string) ?? '';
  return (
    <div className="relative w-full sm:w-72">
      <Search
        aria-hidden
        className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"
      />
      <Input
        type="search"
        aria-label="Buscar cliente"
        placeholder="Buscar por nome…"
        value={value}
        onChange={(event) => table.getColumn('name')?.setFilterValue(event.target.value)}
        className="pl-8"
      />
    </div>
  );
}

export default function DataTableDefault() {
  return (
    <div className="flex w-full justify-center">
      <Card className="w-full max-w-3xl">
        <CardHeader className="flex flex-row items-start justify-between gap-3 space-y-0">
          <div className="flex flex-col gap-1">
            <CardTitle className="text-base">Clientes ativos</CardTitle>
            <p className="text-xs text-muted-foreground">
              {customers.length} clientes neste workspace.
            </p>
          </div>
        </CardHeader>
        <CardContent className="flex flex-col gap-4">
          <DataTable.Provider data={customers} columns={columns} pageSize={5}>
            <DataTable.Toolbar className="justify-start">
              <NameSearch />
            </DataTable.Toolbar>
            <DataTable.Root caption="Clientes ativos no workspace" />
            <DataTable.Pagination />
          </DataTable.Provider>
        </CardContent>
      </Card>
    </div>
  );
}

Uso

import { DataTable } from '@uranus-workspace/blocks';
import type { ColumnDef } from '@tanstack/react-table';

interface Customer {
  id: string;
  name: string;
  email: string;
}

const columns: ColumnDef<Customer>[] = [
  { accessorKey: 'name', header: 'Nome' },
  { accessorKey: 'email', header: 'Email' },
];

<DataTable.Provider data={customers} columns={columns} pageSize={10}>
  <DataTable.Root caption="Lista de clientes" />
  <DataTable.Pagination />
</DataTable.Provider>

Toolbar com acesso à Table

O useDataTable<T>() lê a instância TanStack do contexto, com o T re-aplicado no call-site para tipagem das linhas:

import { DataTable, useDataTable } from '@uranus-workspace/blocks';
import { Input } from '@uranus-workspace/design-system';

function SearchInput() {
  const { table } = useDataTable<Customer>();
  return (
    <Input
      placeholder="Buscar por nome…"
      value={(table.getColumn('name')?.getFilterValue() as string) ?? ''}
      onChange={(event) => table.getColumn('name')?.setFilterValue(event.target.value)}
      className="w-64"
    />
  );
}

<DataTable.Provider data={customers} columns={columns}>
  <DataTable.Toolbar>
    <SearchInput />
  </DataTable.Toolbar>
  <DataTable.Root caption="Clientes" />
  <DataTable.Pagination />
</DataTable.Provider>

Subcomponentes

  • DataTable.Provider — cria o useReactTable e provê o contexto. Aceita data, columns, enableSorting?, enableRowSelection?, pageSize? (padrão 10, 0 desabilita paginação client-side).
  • DataTable.Root — renderiza a tabela. Requer caption (a11y), aceita emptyState?, onRowClick?.
  • DataTable.Toolbar — wrapper composicional com flex gap-2. Children acessam a Table via useDataTable.
  • DataTable.Pagination — controles default ("Anterior" / "Próxima"). Aceita previousLabel? e nextLabel? para custom copy.
  • useDataTable<T>() — hook para custom toolbar/pagination cells.

Acessibilidade

  • caption é obrigatório no DataTable.Root — passamos com sr-only para que a tabela tenha rótulo para leitores de tela.
  • Headers com sort renderizam como <button> dentro do <th>, e o <th> recebe aria-sort="ascending|descending|none".
  • onRowClick adiciona cursor-pointer mas não torna a linha um botão — para ações principais use uma célula com botão real.

Server-side

Para paginação/sorting server-side, passe pageSize={0} para desabilitar a paginação client-side e use useDataTable em uma toolbar custom para refletir suas queries.