← Biblioteca componentes/listas/tabela-comparativa/ficha.md CódigoTela cheia
listas/tabela-comparativa

Tabela comparativa de modelos

🟢 estavel complexidade média prefixo tab-cmp- tabela comparacao matriz criterios modelos honorarios acessibilidade compliance

Incluir · as linhas que se colam num site novo, depois do core (CLAUDE.md §10)

<link rel="stylesheet" href="componentes/listas/tabela-comparativa/estilo.css">
<script src="componentes/listas/tabela-comparativa/script.js"></script>

markup.html · 329 linhas · 19,1 KB

<!-- ==========================================================================
     TABELA COMPARATIVA — três modelos de serviço, critério a critério
     Prefixo: tab-cmp-   (cmp- sozinho já é do midia/comparador-antes-depois,
                          e o dados/comparador-cenarios abriu o precedente ao
                          virar duo-cmp- pelo mesmo motivo)
     Requer: core (tokens, base, motion.css, motion.js) + estilo.css e script.js

     A IDEIA. É uma matriz e é uma <table> de verdade: critérios nas linhas,
     modelos nas colunas, uma marcação em cada célula. Comparação critério a
     critério é dado tabular, e dado tabular tem elemento próprio — grid de
     <div> aqui custaria o cabeçalho de linha, o de coluna e a navegação por
     célula do leitor de tela, sem devolver nada em troca.

     O QUE SE 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"
     é promessa. Nenhuma instituição real é nomeada — as colunas descrevem
     MODELOS. Ver ficha.md, seção Compliance.

     A ALTERNATIVA NÃO É CARICATURA, E ISSO É DECISÃO EDITORIAL. Uma
     comparação em que a outra coluna só tem "não" é propaganda, e o leitor
     sente. Duas linhas existem para impedir isso: em "revisão com data no
     calendário" a distribuição tradicional EMPATA com a casa, e em "sem custo
     direto de assessoria" a autogestão GANHA — e a coluna da casa leva o
     sinal vazio, escrito. É essa linha que dá crédito às outras seis.

     A PREMISSA FICA NA PEÇA, não na ficha: .tab-cmp-premissa diz o que está
     sendo comparado (modelo de serviço e de remuneração) e o que não está
     (instituição e resultado). A tabela aponta para ela por aria-describedby,
     e o <caption> nomeia a tabela para quem ouve a página.

     SINAL SEMPRE COM TEXTO. O disco (cheio / meio / vazio) é aria-hidden e
     serve ao olho que varre a coluna; ao lado dele, sempre, a palavra
     ("Previsto", "Depende", "Não previsto") e a frase curta. Ícone sozinho
     não é lido por leitor de tela nem por quem não separa as cores — e os
     três discos se separam por QUANTIDADE DE PREENCHIMENTO, não por matiz,
     o que os mantém legíveis em tela monocromática.

     NO CELULAR A TABELA VIRA CARDS, um por coluna. O rótulo de cada valor
     vem de data-tab-cmp-criterio lido por content:attr() — o bloco não é
     escrito duas vezes. O <th scope="row"> continua no HTML, invisível mas
     dentro da árvore de acessibilidade, para o leitor de tela seguir
     anunciando o critério de cada célula. Como a virada usa display:contents
     nas linhas (e isso apaga a semântica nativa de tabela), os papéis vão
     ESCRITOS: role="table", "rowgroup", "row", "columnheader", "rowheader",
     "cell". Ver ficha.md, seção Gotchas.

     A LINHA ACENDE INTEIRA no hover — em tabela larga o olho perde a
     horizontal. Em @media(hover:none) isso não existe e nada se perde. E a
     coluna de critério é sticky: quando a tabela rola na horizontal, ela é a
     única que fica, porque uma célula no meio da rolagem sem o critério à
     vista não diz nada.

     A ENTRADA VEM DO CORE, e a escada é do componente. Cada <tr> leva
     data-rv; o degrau de atraso vem de data-tab-cmp-passo="1..7" e não de
     data-d, porque o core só escalona três e misturar as duas fontes deixaria
     a ordem de carregamento decidindo (ver o estilo.css). O .tab-cmp-rolo
     também leva data-rv: no celular as <tr> viram display:contents e o reveal
     delas deixa de existir — é o invólucro que entra.

     TABINDEX QUE NASCE E É REMOVIDO. O contêiner de rolagem começa
     focalizável e o script tira o atributo quando não há o que rolar. Sem
     JavaScript sobra uma parada de tabulação, que é o lado seguro do erro.

     CONTEÚDO ILUSTRATIVO. As descrições dos três modelos são exemplo
     editorial e valem para o modelo em geral, não para uma casa específica.
     Revisar com o compliance do cliente antes de publicar.
     ========================================================================== -->

<section class="sec-deep sec-pad tab-cmp" data-tab-cmp>
  <div class="wrap">

    <div class="sec-head">
      <p class="eyebrow" data-rv>Modelos de relacionamento</p>
      <h2 class="sec-title ttl">Três formas de cuidar do mesmo <em>patrimônio</em></h2>
      <p class="sec-lead" data-rv data-d="1">
        Nenhuma delas é errada. Elas cobram coisas diferentes — do seu bolso, do seu
        tempo e da sua atenção. Abaixo, o que muda em cada uma.
      </p>
    </div>

    <!-- A premissa fica visível, e é ela que a tabela referencia. Comparação
         que esconde o que está comparando é propaganda, não ferramenta. -->
    <p class="tab-cmp-premissa" id="tab-cmp-premissa" data-rv data-d="2">
      O que se compara aqui é <strong>modelo de serviço e de remuneração</strong> —
      não instituição, e não resultado.
    </p>

    <!-- Contêiner de rolagem. Nasce focalizável para quem navega só por
         teclado conseguir rolar a tabela em tela média; o script tira o
         tabindex quando não há o que rolar, para não deixar uma parada de
         tabulação vazia. Sem script, sobra a parada — que é o lado seguro. -->
    <div class="tab-cmp-rolo" data-tab-cmp-rolo data-rv data-d="3" tabindex="0" role="region"
         aria-label="Comparação de modelos de serviço, tabela rolável na horizontal">

      <table class="tab-cmp-tabela" role="table" aria-describedby="tab-cmp-premissa">
        <caption class="tab-cmp-cap visually-hidden">
          Modelos de serviço e de remuneração, critério a critério.
        </caption>

        <thead class="tab-cmp-thead" role="rowgroup">
          <tr class="tab-cmp-linha" role="row" data-rv>
            <th class="tab-cmp-canto" scope="col" role="columnheader">Critério</th>

            <th class="tab-cmp-coluna tab-cmp-coluna-1 tab-cmp-forte" scope="col" role="columnheader">
              <span class="tab-cmp-coluna-nome">Assessoria independente</span>
              <span class="tab-cmp-coluna-nota">taxa previamente acordada</span>
            </th>
            <th class="tab-cmp-coluna tab-cmp-coluna-2" scope="col" role="columnheader">
              <span class="tab-cmp-coluna-nome">Distribuição tradicional</span>
              <span class="tab-cmp-coluna-nota">comissão embutida no produto</span>
            </th>
            <th class="tab-cmp-coluna tab-cmp-coluna-3" scope="col" role="columnheader">
              <span class="tab-cmp-coluna-nome">Autogestão</span>
              <span class="tab-cmp-coluna-nota">você, sozinho</span>
            </th>
          </tr>
        </thead>

        <tbody class="tab-cmp-tbody" role="rowgroup">

          <!-- ------------------------------------------------------ 01 -->
          <tr class="tab-cmp-linha" role="row" data-rv data-tab-cmp-passo="1">
            <th class="tab-cmp-criterio" scope="row" role="rowheader">Remuneração declarada em contrato</th>

            <td class="tab-cmp-cel tab-cmp-cel-1 tab-cmp-forte" role="cell" data-tab-cmp-criterio="Remuneração">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-sim" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Previsto</span>
              </span>
              <span class="tab-cmp-valor">Percentual do patrimônio sob assessoria, acordado antes e escrito no contrato.</span>
            </td>
            <td class="tab-cmp-cel tab-cmp-cel-2" role="cell" data-tab-cmp-criterio="Remuneração">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-nao" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Não previsto</span>
              </span>
              <span class="tab-cmp-valor">Quem remunera é o emissor do produto, não você — e o valor não está na sua fatura.</span>
            </td>
            <td class="tab-cmp-cel tab-cmp-cel-3" role="cell" data-tab-cmp-criterio="Remuneração">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-nao" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Não previsto</span>
              </span>
              <span class="tab-cmp-valor">Não há terceiro remunerado — e também não há a quem perguntar.</span>
            </td>
          </tr>

          <!-- ------------------------------------------------------ 02 -->
          <tr class="tab-cmp-linha" role="row" data-rv data-tab-cmp-passo="2">
            <th class="tab-cmp-criterio" scope="row" role="rowheader">Livre de comissão por produto</th>

            <td class="tab-cmp-cel tab-cmp-cel-1 tab-cmp-forte" role="cell" data-tab-cmp-criterio="Comissão por produto">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-sim" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Previsto</span>
              </span>
              <span class="tab-cmp-valor">A receita do assessor não muda conforme o produto escolhido.</span>
            </td>
            <td class="tab-cmp-cel tab-cmp-cel-2" role="cell" data-tab-cmp-criterio="Comissão por produto">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-nao" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Não previsto</span>
              </span>
              <span class="tab-cmp-valor">A receita muda conforme o produto — é assim que o modelo se sustenta.</span>
            </td>
            <td class="tab-cmp-cel tab-cmp-cel-3" role="cell" data-tab-cmp-criterio="Comissão por produto">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-meio" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Depende</span>
              </span>
              <span class="tab-cmp-valor">Ninguém ganha por indicar. O produto ainda pode ter custo embutido, e não há quem aponte.</span>
            </td>
          </tr>

          <!-- ------------------------------------------------------ 03 -->
          <tr class="tab-cmp-linha" role="row" data-rv data-tab-cmp-passo="3">
            <th class="tab-cmp-criterio" scope="row" role="rowheader">Política de investimentos escrita</th>

            <td class="tab-cmp-cel tab-cmp-cel-1 tab-cmp-forte" role="cell" data-tab-cmp-criterio="Política de investimentos">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-sim" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Previsto</span>
              </span>
              <span class="tab-cmp-valor">Documento com faixas por classe de ativo, decidido em comitê e revisto por lá.</span>
            </td>
            <td class="tab-cmp-cel tab-cmp-cel-2" role="cell" data-tab-cmp-criterio="Política de investimentos">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-meio" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Depende</span>
              </span>
              <span class="tab-cmp-valor">Costuma existir, e costuma nascer da grade de produtos disponível na casa.</span>
            </td>
            <td class="tab-cmp-cel tab-cmp-cel-3" role="cell" data-tab-cmp-criterio="Política de investimentos">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-nao" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Não previsto</span>
              </span>
              <span class="tab-cmp-valor">Existe se você escrever. A maior parte das carteiras nunca teve uma.</span>
            </td>
          </tr>

          <!-- ------------------------------------------------------ 04 -->
          <!-- A linha do empate. A distribuição tradicional atende a este
               critério tão bem quanto a casa, e a peça diz isso. -->
          <tr class="tab-cmp-linha" role="row" data-rv data-tab-cmp-passo="4">
            <th class="tab-cmp-criterio" scope="row" role="rowheader">Revisão com data no calendário</th>

            <td class="tab-cmp-cel tab-cmp-cel-1 tab-cmp-forte" role="cell" data-tab-cmp-criterio="Revisão">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-sim" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Previsto</span>
              </span>
              <span class="tab-cmp-valor">Reunião trimestral marcada, com relatório consolidado enviado antes.</span>
            </td>
            <td class="tab-cmp-cel tab-cmp-cel-2" role="cell" data-tab-cmp-criterio="Revisão">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-sim" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Previsto</span>
              </span>
              <span class="tab-cmp-valor">Há contato periódico, na frequência que a casa define.</span>
            </td>
            <td class="tab-cmp-cel tab-cmp-cel-3" role="cell" data-tab-cmp-criterio="Revisão">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-nao" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Não previsto</span>
              </span>
              <span class="tab-cmp-valor">Acontece quando sobra tempo — em geral depois que o mercado já se mexeu.</span>
            </td>
          </tr>

          <!-- ------------------------------------------------------ 05 -->
          <tr class="tab-cmp-linha" role="row" data-rv data-tab-cmp-passo="5">
            <th class="tab-cmp-criterio" scope="row" role="rowheader">Sucessão e estrutura desenhadas antes</th>

            <td class="tab-cmp-cel tab-cmp-cel-1 tab-cmp-forte" role="cell" data-tab-cmp-criterio="Sucessão">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-sim" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Previsto</span>
              </span>
              <span class="tab-cmp-valor">Holding, previdência e seguro entram no plano, com o advogado e o contador da família.</span>
            </td>
            <td class="tab-cmp-cel tab-cmp-cel-2" role="cell" data-tab-cmp-criterio="Sucessão">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-meio" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Depende</span>
              </span>
              <span class="tab-cmp-valor">Existe onde a casa mantém a área. Costuma entrar depois do produto, não antes.</span>
            </td>
            <td class="tab-cmp-cel tab-cmp-cel-3" role="cell" data-tab-cmp-criterio="Sucessão">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-nao" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Não previsto</span>
              </span>
              <span class="tab-cmp-valor">Vira assunto do inventário — e aí já não é mais planejamento.</span>
            </td>
          </tr>

          <!-- ------------------------------------------------------ 06 -->
          <tr class="tab-cmp-linha" role="row" data-rv data-tab-cmp-passo="6">
            <th class="tab-cmp-criterio" scope="row" role="rowheader">Exige pouco tempo seu por mês</th>

            <td class="tab-cmp-cel tab-cmp-cel-1 tab-cmp-forte" role="cell" data-tab-cmp-criterio="Seu tempo">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-sim" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Previsto</span>
              </span>
              <span class="tab-cmp-valor">Uma conversa por trimestre e um relatório para ler.</span>
            </td>
            <td class="tab-cmp-cel tab-cmp-cel-2" role="cell" data-tab-cmp-criterio="Seu tempo">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-meio" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Depende</span>
              </span>
              <span class="tab-cmp-valor">Pouco tempo, mas parte dele vai para avaliar a oferta da vez.</span>
            </td>
            <td class="tab-cmp-cel tab-cmp-cel-3" role="cell" data-tab-cmp-criterio="Seu tempo">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-nao" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Não previsto</span>
              </span>
              <span class="tab-cmp-valor">Pesquisa, execução, imposto e controle ficam com você, todo mês.</span>
            </td>
          </tr>

          <!-- ------------------------------------------------------ 07 -->
          <!-- A linha em que a casa PERDE, e ela está aqui de propósito: sem
               ela a peça inteira lê como propaganda. O custo da assessoria
               existe, aparece na fatura, e a autogestão não o tem. -->
          <tr class="tab-cmp-linha" role="row" data-rv data-tab-cmp-passo="7">
            <th class="tab-cmp-criterio" scope="row" role="rowheader">Sem custo direto de assessoria</th>

            <td class="tab-cmp-cel tab-cmp-cel-1 tab-cmp-forte" role="cell" data-tab-cmp-criterio="Custo direto">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-nao" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Não previsto</span>
              </span>
              <span class="tab-cmp-valor">Há custo, e ele aparece: a taxa acordada está na fatura, todo mês.</span>
            </td>
            <td class="tab-cmp-cel tab-cmp-cel-2" role="cell" data-tab-cmp-criterio="Custo direto">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-nao" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Não previsto</span>
              </span>
              <span class="tab-cmp-valor">Não há fatura, o que não quer dizer que não há custo: ele está dentro do produto.</span>
            </td>
            <td class="tab-cmp-cel tab-cmp-cel-3" role="cell" data-tab-cmp-criterio="Custo direto">
              <span class="tab-cmp-topo">
                <span class="tab-cmp-marca tab-cmp-marca-sim" aria-hidden="true"></span>
                <span class="tab-cmp-sinal">Previsto</span>
              </span>
              <span class="tab-cmp-valor">Não existe. É a vantagem real deste modelo, e ela é do seu bolso.</span>
            </td>
          </tr>

        </tbody>
      </table>
    </div>

    <!-- A ressalva fica onde a pessoa lê a comparação, não no rodapé. -->
    <p class="nota tab-cmp-nota" data-rv>
      Nenhuma linha compara rentabilidade: ela depende de mercado, de prazo e da carteira
      de cada família — não do modelo. As descrições valem para o modelo em geral; cada
      casa opera de um jeito, e a pergunta que resolve é sempre a mesma: como você é
      remunerado?
    </p>

  </div>
</section>

estilo.css · 306 linhas · 12,9 KB

/* ==========================================================================
   TABELA COMPARATIVA — prefixo tab-cmp-
   Matriz de três modelos de serviço: critérios nas linhas, modelos nas
   colunas, um disco de estado e uma frase curta em cada célula. Compara
   MODELO e CUSTO, nunca resultado (ver o cabeçalho do markup.html).

   O prefixo é tab-cmp- e não cmp-: cmp- já é do midia/comparador-antes-depois,
   e duas .cmp-linha na mesma página colidiriam em silêncio. O
   dados/comparador-cenarios resolveu o mesmo problema virando duo-cmp-.

   Tokens usados:
     --max --pad          só via .wrap do core — nada aqui mexe na coluna
     --sec-gap            respiro da dobra, via .sec-pad
     --display --sans     tipografia
     --ease               curva única do projeto
     --radius-media       canto do card no celular
     --radius-pill        o disco de estado
     --accent             filete e tinta da coluna em destaque, disco cheio
     --bg --fg --fg-2 --fg-muted --rule --rule-2   do escopo .sec-deep da seção

   É UMA <table> DE VERDADE, e por isso este arquivo estiliza CÉLULA POR
   CLASSE em vez de por elemento: 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. São poucas linhas a mais.

   O DISCO SEPARA POR PREENCHIMENTO, NÃO POR MATIZ (cheio, meio, vazio).
   Quem não distingue as cores continua lendo os três estados — e ao lado
   dele há sempre a palavra, no markup.
   ========================================================================== */

.tab-cmp{
  /* Abaixo desta largura a tabela rola dentro do próprio contêiner em vez
     de espremer quatro colunas de texto até a ilegibilidade. É também o que
     faz o .tab-cmp-rolo ganhar foco de teclado em tela média.

     O número tem de ser MAIOR que a coluna disponível na largura em que os
     cards acabam (760px), senão a faixa de rolagem não existe: entre 761px e
     ~900px de janela a coluna do .wrap fica entre 685 e 780px, e é aí que
     esta medida decide se a tabela rola ou se as células espremem. */
  --tab-cmp-min:760px;

  --tab-cmp-pad-x:clamp(12px,1.4vw,22px);
  --tab-cmp-pad-y:clamp(14px,1.5vw,20px);
  --tab-cmp-dot:15px;                        /* diâmetro do disco de estado */

  /* Tinta da coluna em destaque, e a mesma tinta um passo acima no hover.
     Percentual sobre transparente: funciona igual em .sec-dark e .sec-paper. */
  --tab-cmp-forte-bg:color-mix(in srgb,var(--accent) 6%,transparent);
  --tab-cmp-forte-on:color-mix(in srgb,var(--accent) 13%,transparent);

  /* A linha acesa. A versão OPACA existe para a primeira coluna, que é
     sticky: translúcida, o conteúdo rolado apareceria por baixo dela. */
  --tab-cmp-linha-on:color-mix(in srgb,var(--fg) 6%,transparent);
  --tab-cmp-fixa-on:color-mix(in srgb,var(--fg) 6%,var(--bg));

  --tab-cmp-card-gap:clamp(18px,5vw,26px);   /* vão entre cards, no celular */
}

/* ============================================================ premissa e nota */
/* A premissa é o alvo do aria-describedby da tabela: dizer o que está sendo
   comparado é parte da peça, não nota de rodapé. */
.tab-cmp-premissa{
  margin:clamp(28px,3.4vw,46px) 0 clamp(16px,1.8vw,24px);
  max-width:70ch;
  font-size:14px;line-height:1.6;color:var(--fg-2);
}
/* Tipografia pela primitiva .nota do core; margem e medida ficam aqui — 78ch
   acompanha a largura da tabela que a ressalva cobre. */
.tab-cmp-nota{
  margin:clamp(18px,2vw,28px) 0 0;
  --nota-max:78ch;
}

/* ============================================================ contêiner */
/* Rola na horizontal quando a tabela não cabe. Nasce com tabindex="0" no
   markup (quem navega só por teclado precisa alcançar a caixa para rolá-la);
   o script.js tira o tabindex quando não há transbordo. */
.tab-cmp-rolo{overflow-x:auto}

/* ============================================================ tabela */
.tab-cmp-tabela{
  width:100%;
  min-width:var(--tab-cmp-min);
  /* separate e não collapse: com collapse as bordas passam a ser da TABELA,
     e uma <tr> que carrega filter (o [data-rv] do core termina em blur(0),
     que continua sendo filtro) pinta a linha numa superfície própria — as
     bordas colapsadas saem serrilhadas na costura. Com separate a borda é da
     célula e acompanha a linha. */
  border-collapse:separate;
  border-spacing:0;
  /* fixed: as quatro colunas param de dançar conforme o texto de cada
     célula. A largura sai só da primeira linha, e é a do .tab-cmp-canto. */
  table-layout:fixed;
  text-align:left;
}

/* ---- cabeçalho de coluna ---- */
.tab-cmp-canto,
.tab-cmp-coluna{
  padding:0 var(--tab-cmp-pad-x) clamp(12px,1.3vw,18px);
  vertical-align:bottom;
  font-weight:400;
  border-bottom:1px solid var(--rule-2);
}
.tab-cmp-canto{
  width:23%;
  padding-left:0;
  font-family:var(--sans);
  font-size:11px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--fg-muted);
  /* A coluna de critério fica parada enquanto o resto rola: sem ela à vista,
     uma célula no meio da rolagem não diz de que critério é. */
  position:sticky;left:0;z-index:2;
  background:var(--bg);
}
.tab-cmp-coluna-nome{
  display:block;
  font-family:var(--display);font-weight:700;letter-spacing:-.01em;
  font-size:clamp(14px,1.15vw,16px);line-height:1.25;
  color:var(--fg);
}
.tab-cmp-coluna-nota{
  display:block;margin-top:6px;
  font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--fg-muted);
}

/* ---- cabeçalho de linha ---- */
.tab-cmp-criterio{
  padding:var(--tab-cmp-pad-y) var(--tab-cmp-pad-x) var(--tab-cmp-pad-y) 0;
  vertical-align:top;
  font-family:var(--sans);font-weight:400;
  font-size:14px;line-height:1.4;color:var(--fg);
  border-bottom:1px solid var(--rule);
  position:sticky;left:0;z-index:1;
  background:var(--bg);
  transition:background .28s var(--ease);
}

/* ---- célula ---- */
.tab-cmp-cel{
  padding:var(--tab-cmp-pad-y) var(--tab-cmp-pad-x);
  vertical-align:top;
  border-bottom:1px solid var(--rule);
  transition:background .28s var(--ease);
}
.tab-cmp-cel-3{padding-right:0}

/* ---- disco de estado + palavra ---- */
/* O flex mora num <span> dentro da célula, nunca na própria célula: dar
   display:flex a um <td> tira dele o display:table-cell e a tabela inteira
   desmonta. */
.tab-cmp-topo{display:flex;align-items:center;gap:9px}
.tab-cmp-marca{
  flex:0 0 auto;
  width:var(--tab-cmp-dot);height:var(--tab-cmp-dot);
  border-radius:var(--radius-pill);
  border:1px solid var(--accent);
}
.tab-cmp-marca-sim{background:var(--accent)}
.tab-cmp-marca-meio{background:linear-gradient(90deg,var(--accent) 0 50%,transparent 50%)}
.tab-cmp-marca-nao{border-color:var(--rule-2);background:none}
.tab-cmp-sinal{
  font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--fg-muted);
}
.tab-cmp-valor{
  display:block;margin-top:9px;
  font-size:13.5px;line-height:1.5;color:var(--fg-2);
}

/* ---- coluna em destaque ---- */
/* Mover o destaque para outra coluna é mover a classe .tab-cmp-forte no
   markup — nada aqui conhece a posição dela. */
.tab-cmp-forte{background:var(--tab-cmp-forte-bg)}
.tab-cmp-coluna.tab-cmp-forte{border-top:2px solid var(--accent)}

/* ============================================================ linha acesa */
/* Em tabela larga o olho perde a horizontal entre a primeira coluna e a
   última. A linha inteira acende — inclusive a coluna sticky, que precisa da
   versão opaca da tinta para continuar cobrindo o que passa por baixo.
   :focus-within cobre o caso de a célula ganhar um link no futuro.
   Fora de @media(hover:hover) isto viraria estado grudado no toque. */
@media(hover:hover){
  .tab-cmp-linha:hover .tab-cmp-cel,
  .tab-cmp-linha:focus-within .tab-cmp-cel{background:var(--tab-cmp-linha-on)}
  .tab-cmp-linha:hover .tab-cmp-criterio,
  .tab-cmp-linha:focus-within .tab-cmp-criterio{background:var(--tab-cmp-fixa-on)}
  /* depois das duas de cima, de propósito: mesma especificidade, e a coluna
     em destaque tem de continuar distinta com a linha acesa. */
  .tab-cmp-linha:hover .tab-cmp-forte,
  .tab-cmp-linha:focus-within .tab-cmp-forte{background:var(--tab-cmp-forte-on)}
}

/* ============================================================ escalonamento */
/* O core escalona três degraus ([data-rv][data-d="1..3"]) e a tabela tem
   sete linhas. Aqui a escada continua, no MESMO passo de 80ms, por atributo
   próprio — e por atributo, não por classe, para o seletor valer (0,2,0) e
   empatar com o do core em vez de perder para ele.

   Por que não usar data-d nas três primeiras e este nas outras quatro: as
   duas regras teriam a mesma especificidade e a ordem de carregamento
   decidiria — que é exatamente a dependência que o §6.4 proíbe. Ou tudo
   daqui, ou tudo do core. */
.tab-cmp-linha[data-tab-cmp-passo="1"]{transition-delay:.08s}
.tab-cmp-linha[data-tab-cmp-passo="2"]{transition-delay:.16s}
.tab-cmp-linha[data-tab-cmp-passo="3"]{transition-delay:.24s}
.tab-cmp-linha[data-tab-cmp-passo="4"]{transition-delay:.32s}
.tab-cmp-linha[data-tab-cmp-passo="5"]{transition-delay:.40s}
.tab-cmp-linha[data-tab-cmp-passo="6"]{transition-delay:.48s}
.tab-cmp-linha[data-tab-cmp-passo="7"]{transition-delay:.56s}

/* ==========================================================================
   MOBILE — no fim do arquivo, como manda o §6.5

   A TABELA VIRA CARDS, UM POR COLUNA. O DOM é por linha (critério, valor,
   valor, valor) e o card é por coluna — ou seja, a virada é uma TRANSPOSIÇÃO.
   Ela sai de três peças, e nenhuma delas precisa saber quantas linhas a
   tabela tem:

     1. display:contents em <thead>, <tbody> e <tr> derruba as caixas de
        linha, e as células viram filhas diretas do grid da <table>;
     2. order:1|2|3 por coluna agrupa as células da mesma coluna — itens de
        mesmo `order` mantê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;
     3. o rótulo de cada valor vem de content:attr(data-tab-cmp-criterio),
        sem duplicar o bloco no markup.

   O CARD NÃO É UMA CAIXA, é a costura de suas peças: a borda de cima e as
   laterais nascem no cabeçalho da coluna, as laterais seguem nas células, e
   a última linha fecha embaixo. Grupo de itens de grid não tem como ganhar
   um contêiner só para si sem markup a mais.

   O PREÇO DA TRANSPOSIÇÃO, e ele é real: display:contents apaga a semântica
   NATIVA de tabela para a tecnologia assistiva. Por isso os papéis estão
   escritos no markup (role="table"/"rowgroup"/"row"/"columnheader"/
   "rowheader"/"cell") — sem eles, no celular a matriz viraria uma pilha de
   textos soltos. E o <th scope="row"> não é escondido com display:none: ele
   sai do olho pela receita do .visually-hidden e CONTINUA na árvore, para o
   leitor de tela seguir anunciando o critério de cada célula. A receita está
   escrita aqui, e não reaproveitada do core, porque o caso é de ALTERNAR
   entre visível e escondido — o próprio comentário da primitiva em
   core/base.css diz que ela não cobre isso.

   [data-rv] nas <tr> para de valer aqui, e não há como ser diferente: um
   elemento display:contents não gera caixa, e opacity/filter/transform não
   têm onde pegar. Quem entra no celular é o .tab-cmp-rolo, que também leva
   data-rv — então a dobra continua entrando com desfoque, só sem a escada.
   ========================================================================== */

@media(max-width:760px){

  .tab-cmp-rolo{overflow-x:visible}

  .tab-cmp-tabela{
    display:grid;
    grid-template-columns:minmax(0,1fr);
    min-width:0;
    table-layout:auto;
  }
  .tab-cmp-thead,
  .tab-cmp-tbody,
  .tab-cmp-linha{display:contents}

  /* fora do olho, dentro da árvore de acessibilidade */
  .tab-cmp-canto,
  .tab-cmp-criterio{
    position:absolute;
    width:1px;height:1px;
    margin:-1px;padding:0;border:0;
    overflow:hidden;white-space:nowrap;
    clip-path:inset(50%);
  }

  .tab-cmp-coluna-1,.tab-cmp-cel-1{order:1}
  .tab-cmp-coluna-2,.tab-cmp-cel-2{order:2}
  .tab-cmp-coluna-3,.tab-cmp-cel-3{order:3}

  .tab-cmp-coluna{
    padding:clamp(16px,4vw,22px) clamp(16px,4vw,22px) clamp(14px,3.4vw,18px);
    border:1px solid var(--rule);
    border-radius:var(--radius-media) var(--radius-media) 0 0;
    margin-top:var(--tab-cmp-card-gap);
  }
  /* o primeiro card encosta na premissa; o vão só separa um card do outro */
  .tab-cmp-coluna-1{margin-top:0}

  .tab-cmp-cel{
    padding:clamp(14px,3.6vw,18px) clamp(16px,4vw,22px);
    border:1px solid var(--rule);
    border-top:0;
  }
  .tab-cmp-linha:last-child .tab-cmp-cel{
    border-radius:0 0 var(--radius-media) var(--radius-media);
  }

  /* o rótulo que o cabeçalho de linha deixou de mostrar */
  .tab-cmp-cel::before{
    content:attr(data-tab-cmp-criterio);
    display:block;margin-bottom:10px;
    font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;
    color:var(--fg-muted);
  }
  .tab-cmp-valor{font-size:14px}
}

script.js · 70 linhas · 2,7 KB

/* ==========================================================================
   TABELA COMPARATIVA — prefixo tab-cmp-
   Faz UMA coisa só: acerta o foco de teclado do contêiner que rola.

   A IDEIA. Uma caixa com overflow rolável é alcançável pelo mouse e pela
   roda, mas quem navega só por teclado precisa que ela receba foco para
   poder rolá-la com as setas. Por isso o .tab-cmp-rolo NASCE com
   tabindex="0" e role="region" no markup: sem JavaScript, o lado seguro é
   ser focalizável a mais, não a menos.

   O QUE ESTE SCRIPT FAZ É O CONTRÁRIO DO ESPERADO: ele TIRA o tabindex
   quando a tabela cabe inteira na caixa. Uma parada de tabulação que não
   rola nada e não tem nada dentro é ruído para quem depende do Tab — e no
   desktop largo, e no celular (onde a tabela vira cards), é exatamente esse
   o caso. Só sobra foco na faixa do meio, onde a tabela de fato rola.

   NUNCA TIRA O TABINDEX DE QUEM ESTÁ COM O FOCO. Remover o atributo do
   elemento focado joga o foco para o <body> — a pessoa redimensiona a janela
   e perde o lugar na página. Nesse caso o atributo fica, e o próximo
   redimensionamento resolve.

   MOVIMENTO REDUZIDO: não há o que desligar. Este script não anima nada e
   não lê o relógio — o requestAnimationFrame aqui serve só para agrupar
   medições de layout depois de um resize, e por isso não consulta
   LIB.reduce. Toda a animação da peça é do core (data-rv) e do CSS.
   ========================================================================== */

(function () {

  var raizes = document.querySelectorAll('[data-tab-cmp]');
  if (!raizes.length) return;                    // guard: a peça não está aqui

  /* Busca a partir da raiz de cada peça, e não do documento: a mesma tabela
     pode aparecer duas vezes na mesma página. */
  var rolos = [];
  for (var i = 0; i < raizes.length; i++) {
    var alvo = raizes[i].querySelector('[data-tab-cmp-rolo]');
    if (alvo) rolos.push(alvo);
  }
  if (!rolos.length) return;

  var agendado = false;

  function ajustar() {
    agendado = false;
    for (var i = 0; i < rolos.length; i++) {
      var el = rolos[i];
      /* 1px de folga: com zoom de página o scrollWidth vem fracionário e
         arredonda para um pixel a mais sem que nada role de verdade. */
      var rola = el.scrollWidth - el.clientWidth > 1;
      if (rola) {
        el.setAttribute('tabindex', '0');
        el.setAttribute('role', 'region');
      } else if (document.activeElement !== el) {
        el.removeAttribute('tabindex');
        el.removeAttribute('role');
      }
    }
  }

  function agendar() {
    if (agendado) return;
    agendado = true;
    requestAnimationFrame(ajustar);
  }

  addEventListener('resize', agendar, { passive: true });
  ajustar();

})();

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

Quando não usar


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


Gotchas


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:

  1. 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.
  2. 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.
  3. 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.
  4. A ressalva fica onde a pessoa lê o dado — imediatamente abaixo da tabela, não no rodapé da página.
  5. 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:

  1. <table> semântica em vez de grid de div. O padrão de mercado usa div porque o CSS fica mais fácil; o custo é a acessibilidade inteira da peça.
  2. 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.
  3. 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.
  4. 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.