Skip to content
Versão preliminar. Cores, componentes e layout ainda vão mudar: falta receber o padrão de interface do TJES (template-basico-vue) e o manual de identidade visual.

BaseDataTable ​

Tabela de registros do mesmo tipo, para comparar pelas mesmas colunas, achar um registro e agir sobre ele. Ordena pelas colunas, seleciona linhas, pagina e mostra os estados de carregando e vazio. Abaixo de 768 px, cada linha vira um cartão.

Contratos de obra, colunas com botão podem ser ordenadas
SituaçãoAções
CF014/2024
Fórum de Linhares
Engenharia Modelo S.A.
R$ 32.100.000,00Em dia
CF001/2022
Fórum de Alegre
Construtora Exemplo Ltda.
R$ 14.862.310,40Em atraso
CF003/2021
Fórum de Cachoeiro de Itapemirim
Construtora Exemplo Ltda.
R$ 9.105.442,17Concluída
CF007/2023
Reforma do Fórum de Serra
Obras Teste Ltda.
R$ 7.420.115,08Paralisada
CF019/2025
Fórum de Guarapari
Engenharia Modelo S.A.
R$ 5.230.000,00Em atraso

Ordenado por Valor atual, decrescente

Selecionados: nenhum · Ordem: value desc

Uso ​

  • A tabela recebe todos os registros em rows e cuida da ordem e da página. Busca e filtro ficam com a tela: passe em rows só os registros que passaram no filtro.
  • row-key é o campo único de cada linha. A seleção guarda esses valores, então ela sobrevive à ordenação, à troca de página e ao filtro. Veja em Seleção o que acontece com as linhas selecionadas que o filtro esconde.
  • Em TypeScript, o tipo das linhas vem de rows: em #cell-<key> e #actions, row já tem o tipo do registro (row.code é checado).
  • label é obrigatório: vira a legenda (<caption>) da tabela, só para leitores de tela. O título visível fica no cabeçalho da página ou do cartão.
  • A primeira coluna nomeia a linha: ela vira cabeçalho de linha (<th scope="row">) e dá o nome da caixa de seleção, "Selecionar CF001/2022". Use row-header para escolher outra coluna.
  • No máximo 7 colunas. A oitava vira subtítulo de outra coluna (como a empresa abaixo da obra) ou vai para a tela de detalhe.
  • Cabeçalho em frase curta, sem abreviar: "Valor atual", não "Vlr. at.".
  • Sem v-model:page-size, todas as linhas aparecem e o rodapé mostra "7 registros". Com ele, o rodapé ganha a BasePagination e só a página atual aparece.

Colunas e alinhamento ​

Cada coluna é um objeto { key, label, sortable, align, numeric, width }.

DadoComo configurarComo aparece
Texto, nome{ key, label }À esquerda
Dinheiro, quantidade, percentualnumeric: trueÀ direita, algarismos tabulares, sem quebra de linha
CódigoSlot #cell-<key> com a classe tj-datatable-codeÀ esquerda, fonte mono 13 px
Detalhe de outra colunaSlot com a classe tj-datatable-sub na segunda linha13 px em --tj-text-helper, abaixo do texto principal
SituaçãoSlot #cell-status com BaseBadgeSelo com ponto e texto, nunca só a cor

align ('start', 'center' ou 'end') troca o alinhamento quando o padrão não serve. width aceita qualquer medida CSS, como '140px' ou '20%'.

Sem slot, a célula mostra o valor: números no formato brasileiro (14.862.310,4), true e false como "Sim" e "Não", e qualquer outra coisa (datas, objetos, null) vazia. Formate dinheiro e datas no slot #cell-<key>, que recebe { row, value, column }.

Ordenação ​

Itens da medição de outubro, colunas com botão podem ser ordenadas
DescriçãoQuantidade
4.2Alvenaria de vedação892,286.410,2
5.1Chapisco e reboco interno1.24054.118
8.3Porta de veneziana1412.904,35

3 registros

  • Só colunas com sortable: true ganham o botão no cabeçalho. Com pelo menos uma, a legenda ganha, só para leitores de tela, "colunas com botão podem ser ordenadas".
  • O primeiro clique ordena texto de A a Z e números do maior para o menor, porque em valores a pessoa procura primeiro os maiores. Cada clique seguinte inverte a direção.
  • A ordem nova volta para a página 1. A ordenação usa sortRows: estável (empates mantêm a ordem de rows), sem diferenciar maiúsculas e acentos e com números dentro do texto em ordem natural ("Item 2" antes de "Item 10").
  • v-model:sort recebe { key, direction } ou null, para a tela abrir já ordenada ou guardar a ordem na URL.

Seleção ​

Com selectable, cada linha ganha uma caixa de seleção e o cabeçalho ganha "Selecionar todas as linhas da página". Ela fica marcada quando todas as linhas da página estão selecionadas e mista (indeterminate) quando só algumas estão. Marcá-la ou desmarcá-la atua só nas linhas da página atual e mantém a seleção das outras páginas. O rodapé mostra "2 de 7 selecionados".

Uma linha selecionada que o filtro esconde continua em v-model:selected, para a seleção voltar quando o filtro sair. O rodapé conta essas linhas à parte, "1 de 3 selecionados, mais 1 fora do filtro", para a pessoa saber que a seleção vai além do que vê. Ações em lote usam a lista inteira de selected; se a tela preferir agir só no que está à vista, ela tira de selected as chaves que saíram de rows ao filtrar.

A linha selecionada ganha fundo --tj-layer-selected-01 e uma barra azul à esquerda, além da caixa marcada.

Densidade ​

Itens da medição de setembro, colunas com botão podem ser ordenadas
DescriçãoQuantidade
4.2Alvenaria de vedação892,286.410,2
5.1Chapisco e reboco interno1.24054.118
8.3Porta de veneziana1412.904,35

3 registros

density="default" tem linhas de 52 px (--tj-row-default), para listas que a pessoa lê. density="compact" tem linhas de 40 px (--tj-row-compact), para planilhas que a pessoa confere linha a linha, como os itens da medição.

Estados ​

Contratos de obra, colunas com botão podem ser ordenadas
Situação

Carregando registros

Nenhum contrato cadastrado

Os contratos de obra aparecem aqui assim que o primeiro for cadastrado.

  • Carregando (loading): linhas de esqueleto no lugar dos dados (5 por padrão, skeleton-rows muda), a tabela fica com aria-busy="true" e o leitor de tela ouve "Carregando registros", também na primeira carga. Os botões de ordenar e a paginação continuam no foco, mas ignoram cliques; as caixas de seleção ficam desabilitadas.
  • Vazio: sem registros, a BaseEmptyState toma o lugar da tabela e do rodapé, com empty-title, empty-text, empty-icon e o slot #empty-action. Diga o que vai aparecer ali ("Nenhum contrato cadastrado") ou, depois de uma busca, o que não foi achado e como sair dali ("Limpar busca"). O título é um <h3>; empty-heading-level troca o nível para ficar um abaixo do título do cartão ou da seção. O slot #empty troca o estado vazio inteiro; mesmo com ele, o leitor de tela ouve o empty-title, então escreva um que sirva.
  • Erro: mostre a BasePageState no lugar da tabela; a tabela não busca dados e não conhece o erro.

No celular ​

Abaixo de 768 px, cada linha vira um cartão: a caixa de seleção à esquerda, as ações à direita e cada valor com o nome da coluna em cima. A linha de cabeçalho vira uma barra com a caixa e o texto "Selecionar todas" e, depois de "Ordenar por", os botões de ordenar. Sem seleção e sem coluna ordenável, a barra some. A tabela larga em telas maiores rola dentro do próprio contêiner, e a página nunca rola para o lado.

Estilo ​

PropriedadeToken
ContêinerFundo --tj-layer-01, borda --tj-border-subtle-02, raio --tj-radius-lg, sombra --tj-shadow-1
CabeçalhoFundo --tj-layer-02, texto --tj-text-helper 12 px semibold (5,61:1 no tema claro, 6,92:1 no escuro); coluna ordenada em --tj-text-primary
Botão de ordenarHover --tj-layer-hover-01, pressionado --tj-layer-accent-01; ícone --tj-icon-secondary, --tj-icon-primary na coluna ordenada
LinhaAltura --tj-row-default (52 px) ou --tj-row-compact (40 px), divisória --tj-border-subtle-01, hover --tj-layer-hover-01
Linha selecionadaFundo --tj-layer-selected-01, hover --tj-layer-selected-hover-01, barra de 3 px --tj-border-interactive
Cabeçalho de linha--tj-weight-semibold, --tj-text-primary
RodapéFundo --tj-layer-02, texto --tj-text-helper 13 px, espaço --tj-space-3 × --tj-space-4
EspaçosCélula --tj-space-3 × --tj-space-4 (--tj-space-2 na vertical quando compacta); barra de ferramentas --tj-space-4
FocoContorno de 2 px em --tj-border-interactive por dentro do botão de ordenar e da área de rolagem

Diferenças em relação ao protótipo v3, de propósito: o cabeçalho não é sticky, porque a tabela rola para o lado dentro do próprio contêiner e um cabeçalho fixo grudaria nesse contêiner, que não rola na vertical; o hover da linha usa --tj-layer-hover-01, o token de hover sobre --tj-layer-01, em vez de --tj-layer-02, que é a cor do cabeçalho e quase não se distingue do branco (1,03:1); no celular a linha de cabeçalho vira barra em vez de sumir, para ordenar e selecionar a página continuarem possíveis; e não há .is-wide nem .is-hide-sm, porque cada valor do cartão já ocupa a coluna inteira e esconder uma coluna no celular esconderia dado.

Código ​

vue
<BaseDataTable
  v-model:sort="sort"
  v-model:selected="selected"
  v-model:page="page"
  v-model:page-size="pageSize"
  :columns="columns"
  :rows="filteredContracts"
  row-key="id"
  label="Contratos de obra"
  :page-sizes="[10, 25, 50]"
  :loading="loading"
  selectable
>
  <template #toolbar="{ tableId }">
    <BaseSearch v-model="query" label="Buscar por contrato, obra ou empresa" />
    <BaseSegmented v-model="filter" label="Situação" :options="filters" :controls="tableId" />
  </template>
  <template #cell-status="{ value }">
    <BaseBadge :tone="tones[value]">{{ statusText[value] }}</BaseBadge>
  </template>
  <template #actions="{ row }">
    <BaseButton kind="ghost" size="sm" icon="three-dots" icon-only :label="`Ações do contrato ${row.code}`" />
  </template>
</BaseDataTable>
PropriedadeTipoPadrãoDescrição
columnsDataTableColumn[]—Colunas: { key, label, sortable?, align?, numeric?, width? } (obrigatório)
rowsT[]—Todos os registros, já filtrados pela tela (obrigatório); T vira o tipo de row nos slots
rowKeystring—Campo único de cada linha (obrigatório)
labelstring—Nome da tabela, na legenda para leitores de tela (obrigatório)
rowHeaderstringprimeira colunaColuna que nomeia a linha e a caixa de seleção
selectablebooleanfalseColuna de caixas de seleção com "selecionar todas"
loadingbooleanfalseLinhas de esqueleto, tabela ocupada e controles inertes
density'default' | 'compact''default'Linhas de 52 ou de 40 px
pageSizesnumber[]—Opções de "Itens por página"; precisa de v-model:page-size
skeletonRowsnumber5Linhas de esqueleto enquanto carrega
emptyTitlestring'Nenhum registro'Título do estado vazio
emptyTextstring—Texto do estado vazio
emptyIconstring'inbox'Ícone do estado vazio, nome do Bootstrap Icons sem bi-
emptyHeadingLevel2 | 3 | 4 | 5 | 63Nível do título do estado vazio
v-model:sort{ key, direction } | nullnullColuna e direção ('asc' ou 'desc') da ordem
v-model:selected(string | number)[][]Valores de rowKey das linhas selecionadas, inclusive as que o filtro escondeu
v-model:pagenumber1Página atual
v-model:page-sizenumber—Linhas por página; sem ele, não há paginação
SlotPropsDescrição
#toolbar{ tableId }Busca, filtros e ações acima da tabela; passe tableId ao controls da BaseSegmented
#cell-<key>{ row, value, column }Conteúdo próprio das células de uma coluna
#actions{ row }Ações da linha, em geral um BaseButton só com ícone cujo label cita o registro
#empty—Troca o estado vazio inteiro
#empty-action—Ação do estado vazio padrão

Eventos: update:sort, update:selected, update:page e update:pageSize. Uma ordem nova também emite update:page com 1.

Os tipos DataTableColumn, DataTableSort, DataTableDensity e RowKey são exportados pelo pacote.

Acessibilidade ​

  • Segue o padrão de tabela ordenável do WAI-ARIA APG: um <table> de verdade, com <caption>, <th scope="col"> nas colunas e <th scope="row"> na célula que nomeia a linha. Nada de grade de div.
  • A ordenação é um <button> nativo dentro do <th>, com o nome da coluna. Tab chega nele; Enter e Espaço ordenam. Só a coluna ordenada leva aria-sort="ascending" ou "descending". Depois de cada troca, uma região role="status" anuncia "Ordenado por Valor atual, decrescente", porque nem todo leitor de tela fala a mudança de aria-sort.
  • A cor não é o único sinal: o ícone de ordenar muda de forma (setas duplas, seta para cima, seta para baixo), e a linha selecionada tem a caixa marcada e a barra à esquerda, além do fundo.
  • Cada caixa de seleção tem nome próprio, "Selecionar CF001/2022", e a do cabeçalho é "Selecionar todas as linhas da página". O estado misto vem da propriedade indeterminate do <input>, que o navegador expõe como aria-checked="mixed". A área de toque é de 24 px (WCAG 2.5.8).
  • Uma região role="status" sempre presente, escondida da tela, anuncia o que muda: "Carregando registros" (também na primeira carga, porque a mensagem só entra depois que a região existe), a contagem depois de selecionar ou filtrar ("2 de 7 selecionados", "3 registros") e o empty-title quando o filtro zera a lista. Ela fica fora do estado vazio, por isso o anúncio não some junto com a tabela. O texto visível do rodapé não é região viva, para nada ser dito duas vezes. Com paginação, a BasePagination anuncia também o intervalo.
  • Os botões de ação da linha são só ícone e precisam de label que cite o registro: "Ações do contrato CF001/2022". O cabeçalho da coluna de ações tem o texto "Ações" para leitores de tela.
  • Quando a tabela é mais larga que o contêiner, a área de rolagem vira uma região "Contratos de obra, área de rolagem" e entra no Tab (tabindex="0"), para quem usa teclado rolar com as setas (WCAG 2.1.1). Sem rolagem lateral, como no layout de cartões, ela não tem papel nem entra no Tab. Um ResizeObserver confere a largura.
  • Quando o carregamento ou o estado vazio tira da tela o controle que estava com o foco (uma caixa de seleção, um botão de ação, o botão de ordenar), o foco vai para a área da tabela ou para o estado vazio, e não cai no <body> (WCAG 2.4.3).
  • Enquanto carrega, a tabela tem aria-busy="true", o <tbody> do esqueleto tem aria-hidden="true" (a tabela expõe só a linha de cabeçalho) e "Carregando registros" é anunciado. Os botões de ordenar recebem aria-disabled em vez de disabled, para o foco não sumir.
  • O foco do botão de ordenar e da área de rolagem é um contorno sólido em --tj-border-interactive, não o --tj-ring translúcido, que fica abaixo de 3:1 sobre o fundo do cabeçalho (WCAG 1.4.11). No modo de alto contraste do Windows, o contorno e a barra da linha selecionada usam a cor de destaque do sistema.
  • No layout de cartões, os nomes de coluna que aparecem acima dos valores ficam com aria-hidden, porque o leitor já ouve o cabeçalho. A tabela, os grupos, as linhas e as células levam role explícito, para manter a semântica de tabela quando o CSS troca o display delas; essa é a correção conhecida para navegadores que descartam a semântica nesse caso. Os cabeçalhos que não ordenam continuam lá, só escondidos da tela, e os textos "Selecionar todas" e "Ordenar por" da barra têm aria-hidden, porque a caixa e os botões já têm nome.
  • As transições de cor da linha e do botão de ordenar somem com prefers-reduced-motion; o esqueleto segue as regras da BaseSkeleton. Funciona nos temas claro e escuro.
  • Testado com axe nos estados que o jsdom cobre: padrão, ordenada crescente e decrescente, troca de coluna ordenada, seleção nenhuma, parcial e total, seleção com linha fora do filtro, paginada, carregando (com seleção, ações e paginação), primeira carga sem registros, vazia (padrão e com o slot #empty), slots de célula e de ações, cabeçalho de linha em outra coluna, área de rolagem com rolagem lateral, barra de ferramentas com BaseSearch e BaseSegmented e densidade compacta. O layout de cartões, o contraste e o modo de alto contraste dependem de CSS e são conferidos visualmente.