Uma matriz: critérios nas linhas, modelos de serviço nas colunas, e em cada célula um disco de estado (cheio, meio, vazio) com a palavra e uma frase curta. A coluna da casa tem filete na cor de marca e um fundo um passo acima; a linha inteira acende quando o ponteiro passa, para o olho não perder a horizontal. No celular a tabela vira um card por coluna, sem que o markup seja escrito duas vezes.
Ela existe porque comparar critério a critério é dado tabular, e a biblioteca não tinha nenhuma tabela — a dobra seria improvisada com div, que é o erro clássico: custa o cabeçalho de linha, o de coluna, a navegação por célula do leitor de tela e o <caption>, e não devolve nada em troca. O que essa forma resolve editorialmente é o momento em que a página precisa parar de argumentar em prosa e mostrar a comparação inteira de uma vez, com a alternativa escrita de forma reconhecível por quem já vive nela.
Quando usar
- Página de "como trabalhamos", de honorários ou de "por que independente": o ponto da página em que a pessoa está comparando você com o que ela já tem.
- Quando há três ou mais critérios e duas ou três alternativas. Com dois critérios, dois parágrafos resolvem melhor; com mais de quatro colunas, nem a tabela salva.
- Quando cada célula cabe em uma frase. Célula com três linhas de prosa transforma a matriz em texto disfarçado de tabela.
- Quando a comparação é de fato verificável — modelo, custo, escopo, frequência. Ver a seção Compliance.
Quando não usar
- Para vender três pacotes com preço e botão: isso é
conversao/planos-honorarios. A diferença é o eixo — lá cada coluna é uma oferta com sua própria chamada para ação; aqui cada coluna é um modelo do mundo, e não há botão nenhum dentro da matriz. Uma página madura costuma ter as duas, nesta ordem: primeiro a matriz para situar, depois os planos para escolher. - Para comparar números em função de um controle (patrimônio, horizonte): é
dados/comparador-cenarios, que calcula e desenha barras, e cuja tabela é a leitura acessível do gráfico, não a peça. Aqui não há cálculo nenhum: o conteúdo é escrito à mão e revisado por gente. - Para comparar duas imagens do mesmo enquadramento:
midia/comparador-antes-depois. - Para uma lista de diferenciais sem alternativa do outro lado:
conteudo/imagem-lista-numeradaoulistas/acordeao-faqcabem melhor, e sem o peso de uma tabela. - Em página de captação onde não há disposição para nomear a alternativa com honestidade. Uma matriz em que a outra coluna só tem "não" é propaganda, e o leitor sente.
Padrão editorial
| Slot | Regra |
|---|---|
| Olho | Duas ou três palavras que nomeiam o eixo da comparação, não o produto. "Modelos de relacionamento", não "Nossos diferenciais". |
| Título | Uma frase que admita que existem várias formas certas. O realce em <em> cai na palavra que a página inteira defende (patrimônio, custo, independência). |
| Apoio | Duas linhas, e uma delas desarma: "nenhuma delas é errada". O apoio é o lugar de reconhecer a alternativa; a tabela é o lugar de detalhá-la. |
| Premissa | Uma linha, acima da tabela, dizendo o que está sendo comparado e o que não está. É conteúdo obrigatório, não decoração — e a tabela aponta para ela por aria-describedby. |
| Cabeçalho de coluna | Nome do modelo em duas ou três palavras + uma linha em caixa alta com o mecanismo de remuneração. Nunca um nome de instituição. |
| Cabeçalho de linha | O critério escrito como propriedade desejável, nunca como pergunta: "Revisão com data no calendário", não "Com que frequência há revisão?". É o que dá polaridade única ao disco. |
| Célula | Disco + palavra de estado (Previsto / Depende / Não previsto) + uma frase de até 14 palavras. A frase é o dado; o disco é o atalho do olho. |
| Rótulo de celular | data-tab-cmp-criterio: uma ou duas palavras, versão curta do critério. Aparece só no card. |
| Nota | A ressalva, imediatamente abaixo da tabela — onde a pessoa lê a comparação, nunca no rodapé da página. |
Sete linhas é o teto confortável e cinco é o número que mais funciona: acima de sete a matriz vira formulário e o leitor desiste antes do fim. O que mantém a peça coesa é o paralelismo: todo critério na mesma forma gramatical, toda célula com o mesmo comprimento aproximado, e nenhuma frase que precise de vírgula dupla.
A regra que decide a credibilidade da peça: pelo menos uma linha em que a alternativa ganha ou empata, e ela precisa ser uma linha que importa. Aqui são duas, de propósito — na revisão com data marcada a distribuição tradicional empata com a casa, e no custo direto de assessoria a autogestão ganha, com a coluna da casa levando o disco vazio e a frase "há custo, e ele aparece". Sem essa linha, as outras seis não valem nada: o leitor lê a matriz inteira como material de venda e para de conferir. Com ela, ele confere — e é isso que se quer.
Especificação dos assets
Nenhum. A peça é tipografia, filete e três discos desenhados em CSS — sem imagem, sem ícone de biblioteca, sem logo.
Isso é decisão, não economia: logo de instituição real nunca entra numa comparação (ver Compliance), e ícone de check/x colorido carrega exatamente o vício que este componente evita — informação só na cor e só no desenho. Os três estados se separam por quantidade de preenchimento (cheio, meio, vazio), o que sobrevive a tela monocromática e a daltonismo, e cada um vem acompanhado da palavra escrita.
Como usar
<link rel="stylesheet" href="componentes/listas/tabela-comparativa/estilo.css">
<script src="componentes/listas/tabela-comparativa/script.js"></script>
Colar o markup.html dentro de uma dobra de escopo de tom (sec-deep no exemplo; sec-paper e sec-light funcionam sem tocar em nada). Trocar critérios, colunas e frases — e trocar o id="tab-cmp-premissa" se a peça aparecer duas vezes na mesma página, junto do aria-describedby que aponta para ele.
Ganchos
| Gancho | Papel |
|---|---|
[data-tab-cmp] |
Raiz da peça. O script parte daqui, e não do documento — a tabela pode repetir na página. |
[data-tab-cmp-rolo] |
O contêiner que rola na horizontal. Nasce com tabindex="0" e role="region"; o script os remove quando não há transbordo. |
[data-tab-cmp-passo="1..7"] |
Degrau de atraso da entrada de cada linha, 80 ms por degrau. |
[data-tab-cmp-criterio] |
Rótulo curto do valor, lido por content:attr() no card de celular. |
.tab-cmp-forte |
Marca a coluna em destaque — no cabeçalho e em cada célula dela. Nenhuma regra conhece a posição: mover a classe move o destaque. |
.tab-cmp-marca-sim/-meio/-nao |
Os três estados do disco. |
role="table" e família |
Escritos no markup porque a virada para cards no celular usa display:contents. Ver Gotchas. |
Variações
- duas colunas — apagar o
<th>do cabeçalho e a célula.tab-cmp-cel-Nde cada linha. A largura se redistribui sozinha (table-layout:fixedsó fixa a primeira coluna) e nada no CSS conhece a contagem. Estável. - destaque em outra coluna — mover
.tab-cmp-forte. Estável. - sem coluna destacada — retirar
.tab-cmp-fortede tudo. A matriz vira comparação neutra: dois regimes tributários, dois tipos de estrutura societária, previdência aberta contra fechada. Estável. - mais de sete critérios — cada linha nova precisa do próprio
data-tab-cmp-passoe de um degrau novo no fim doestilo.css. Sem o atributo a linha entra sem atraso, junto da primeira, e a escada fica torta. Experimental — nunca passou de sete.
Gotchas
- A célula leva classe, e isso não é preciosismo. O §6.3 do contrato proíbe seletor de elemento solto, e
td{}numa folha que pode ser colada em qualquer posição vazaria para toda tabela da página — inclusive a de outro componente. São vinte linhas a mais de classe e nenhuma linha de CSS a mais. display:flexnum<td>desmonta a tabela. O elemento perde odisplay:table-celle sai da grade de colunas. Por isso o disco e a palavra são um<span class="tab-cmp-topo">dentro da célula, e o flex mora nele.- A virada para cards apaga a semântica nativa de tabela, e por isso os papéis vão escritos. No celular a
<table>viradisplay:gride<thead>,<tbody>e<tr>viramdisplay:contents— sem isso a transposição (linha no DOM, coluna no card) não existe. O preço é que a tecnologia assistiva deixa de derivar a semântica do layout: semrole="table","rowgroup","row","columnheader","rowheader"e"cell"no markup, a matriz viraria uma pilha de textos soltos em quem depende do leitor de tela. Os papéis são idênticos aos nativos, então no desktop não mudam nada. - O
<th scope="row">não é escondido comdisplay:noneno celular. Ele sai do olho pela receita do.visually-hidden— escrita localmente, porque o caso é de alternar entre visível e escondido, e o comentário da primitiva emcore/base.cssdiz explicitamente que ela não cobre isso.display:nonetiraria o cabeçalho da árvore de acessibilidade, e aí o leitor de tela pararia de anunciar o critério de cada célula, que é justamente o que a tabela tem de melhor. O rótulo que o olho vê no card vem de outro lugar:content:attr(data-tab-cmp-criterio). - A transposição não conhece o número de linhas, e é por isso que ela sobrevive a uma edição. Ela sai de
order:1|2|3por coluna: itens de grid com o mesmoordermantêm a ordem do documento entre si, então cada grupo sai na ordem dos critérios, e o cabeçalho da coluna — que vem antes no DOM — encabeça o grupo. Medido em 390px: a sequência visual das 24 peças sai111111112222222233333333. A alternativa seria numerargrid-rowcélula a célula, e aí acrescentar um critério quebraria o layout em silêncio. - O card não é uma caixa: é a costura das peças dele. Um grupo de itens de grid não ganha um contêiner só para si sem markup a mais. A borda de cima e as laterais nascem no cabeçalho da coluna, as laterais seguem nas células, e
.tab-cmp-linha:last-child .tab-cmp-celfecha embaixo. Quem inserir uma linha nova depois da última precisa saber disso — e não precisa fazer nada, porque:last-childacompanha. - A escada de entrada é do componente, não do core, e as duas não se misturam. O core escalona três degraus (
[data-rv][data-d="1..3"]) e a tabela tem sete linhas. Usardata-dnas três primeiras e um atributo próprio nas quatro seguintes daria duas regras de mesma especificidade (0,2,0), e a ordem de carregamento decidiria qual vence — que é exatamente a dependência que o §6.4 proíbe. Ou tudo do core, ou tudo daqui: aqui é tudo daqui, em[data-tab-cmp-passo]. [data-rv]nas linhas para de valer no celular, e não há como ser diferente. Elementodisplay:contentsnão gera caixa, eopacity/filter/transformnão têm onde pegar. Quem entra no celular é o.tab-cmp-rolo, que também levadata-rv— a dobra continua entrando com desfoque, só sem a escada por linha.position:stickyna primeira coluna SOBREVIVE ao filtro residual do reveal — medido, e ao contrário do que a intuição diz. O[data-rv].indo core termina emfilter:blur(0), que continua sendo filtro, e oMANUAL.mddocumenta que isso faz do elemento um bloco de contenção para descendenteposition:fixed. Comstickyo resultado é outro: medido no Chrome em 820px de janela, com a<tr>computandofilter:blur(0px), o<th>da linha ficou parado emleft:41enquanto a célula da terceira coluna andou-37px— exatamente a rolagem aplicada.fixedquebra,stickynão. Não confunda os dois ao ler o manual.--tab-cmp-mintem de ser maior que a coluna disponível na largura em que os cards acabam. Se for menor, a faixa de rolagem simplesmente não existe: a tabela cabe em toda janela acima do ponto de corte, o.tab-cmp-rolonunca transborda, e a parada de tabulação some sem que ninguém perceba que o mecanismo nunca foi exercido. Foi o que aconteceu na primeira medição, com660px: entre 761 e 900px de janela a coluna do.wrapjá era maior que isso. Com760px, a 820px de janela o contêiner mede 723 contra 760 de conteúdo e rola de verdade.- O script tira o
tabindex, não põe. O markup nasce focalizável — sem JavaScript o lado seguro é sobrar foco, não faltar. O script remove o atributo quando a tabela cabe inteira, para não deixar uma parada de tabulação vazia no desktop e no celular. E nunca remove de um elemento que está com o foco: tirar o atributo de quem tem foco joga o foco para o<body>, e a pessoa perde o lugar na página ao redimensionar a janela.
Compliance
Esta peça compara modelos de negócio de assessoria de investimentos. O que ela diz é lido como argumento comercial e cai sob as regras de publicidade do setor. Antes de publicar, o consumidor precisa conferir cinco coisas — e as cinco já estão implementadas aqui, então o trabalho é não desfazê-las:
- Compara modelo e custo, nunca resultado. Nenhuma linha diz nem insinua que um modelo rende mais. "Remuneração por comissão de produto" contra "taxa previamente acordada" é fato verificável; "melhor retorno", "mais rentável" ou "performance superior" é promessa, e não entra em nenhuma célula. A nota abaixo da tabela diz isso por escrito: rentabilidade depende de mercado, de prazo e da carteira de cada família — não do modelo.
- Nenhuma instituição real é nomeada, e nenhum logo aparece. As colunas descrevem modelos ("distribuição tradicional", "autogestão"), não empresas. Trocar o cabeçalho por um nome de banco ou de corretora transforma a peça em publicidade comparativa e muda o regime jurídico dela inteiro.
- A premissa fica visível na peça, acima da tabela, e a tabela aponta para ela por
aria-describedby. Comparação que esconde o que está comparando é propaganda, não ferramenta. - A ressalva fica onde a pessoa lê o dado — imediatamente abaixo da tabela, não no rodapé da página.
- A alternativa não é caricatura. Pelo menos uma linha em que ela ganha ou empata, escrita sem ironia. Ver a nota no Padrão editorial: essa linha é o que dá crédito a todas as outras.
Conteúdo ilustrativo. As descrições dos três modelos são exemplo editorial e valem para o modelo em geral — cada casa opera de um jeito. Revisar frase a frase com o compliance do cliente antes de publicar, e ajustar tudo que descreva a operação da casa (frequência de reunião, escopo de sucessão, o que entra na taxa).
Nada é capturado. A peça não tem campo, não tem localStorage, não manda nada para lugar nenhum. É leitura.
Histórico
| Projeto | Onde | Data | Diferenças |
|---|---|---|---|
| autoral | matriz comparativa de critérios | 2026-09-06 | Construída aqui, sem referência única e sem uma linha copiada. |
De onde veio a forma. O padrão é o da matriz de checagem por critério, que aparece em página de preço de SaaS (comparativo de planos), em página de consultoria (modelo A contra modelo B) e em página de produto financeiro. Nenhuma referência única: o que se pegou dessa família foi a proporção (coluna de critério estreita, colunas de valor iguais), a marcação por célula e o destaque de uma coluna. Quatro decisões foram tomadas na tradução para o contrato:
<table>semântica em vez de grid dediv. O padrão de mercado usadivporque o CSS fica mais fácil; o custo é a acessibilidade inteira da peça.- O sinal separa por preenchimento e nunca aparece sozinho. O padrão de mercado é check verde contra x vermelho, mudo. Aqui é disco cheio / meio / vazio com a palavra ao lado.
- A virada para cards no celular sai de
display:contents+order, sem markup duplicado — e paga o preço com os papéis ARIA escritos no HTML. - O compliance decidiu o conteúdo, não só o texto: nenhuma linha compara resultado, nenhuma instituição é nomeada, a premissa está na peça, e uma linha é perdida pela casa de propósito.
Normalizações aplicadas na extração: prefixo tab-cmp- — e não cmp-, que já é do midia/comparador-antes-depois; o dados/comparador-cenarios abriu o mesmo precedente ao virar duo-cmp-. Cores só por token (--accent, --rule, --rule-2, --fg, --fg-2, --fg-muted, --bg), as tintas de destaque e de linha acesa por color-mix sobre transparente, para funcionarem igual em escopo escuro e claro. Entrada pelo data-rv do core, sem observer próprio. Hover dentro de @media(hover:hover). Media query de largura no fim do arquivo.
Divergências conhecidas vs. produção: nenhuma — a peça nunca rodou em site. O que se sabe dela é o que foi medido: em 1440px as células de cada uma das sete linhas dividem o mesmo top (zero desalinhadas), a estrutura tem 1 <caption>, 4 th[scope=col], 7 th[scope=row] e 21 <td>, o hover acende as quatro células da linha e nenhuma da vizinha, em 820px o contêiner rola 37px e mantém tabindex="0" (a seta do teclado rola até o fim), em 390px a tabela vira três cards com rótulo em todas as 21 células e zero transbordo horizontal, com prefers-reduced-motion nada se move e a altura da seção não muda, e com o JavaScript desligado as sete linhas continuam visíveis e legíveis nas três larguras. Console limpo em todas as medições.