A grade no feitio do app de Ações do iPhone e do painel do Google Finance: um card por papel, com símbolo, nome, minigráfico de área, preço em corpo grande e a variação com seta.
A forma é essa porque é a leitura que o público já sabe fazer. E ela ganha da faixa de cotações correndo em quase toda página institucional por uma razão simples: em card a pessoa procura o papel dela; na faixa, ela espera ele passar.
O que o componente tem além da forma cabe numa frase: o selo "ao vivo" só existe enquanto a leitura está dentro da validade declarada. Vencido o prazo, os preços saem do DOM, a cor de direção some, a borda vira tracejada e um aviso diz desde quando.
Compliance
Esta seção não é conselho editorial. É a razão de o componente existir dessa forma.
Um preço em corpo grande, numa página de instituição financeira, é informação financeira ao público. Ele carrega três afirmações, e nenhuma delas está escrita em lugar nenhum:
- que este é o preço daquele papel;
- que ele é de agora;
- que alguém apurou — não é a conta do site.
O componente responde às três com estrutura, não com texto:
- A
.coto-fonteé obrigatória. Provedor, instante e atraso. Sem provedor e sem instante, o preço acima não é cotação: é um número grande. Mesmo raciocínio que aprova/manchetes-imprensafaz com data e link. data-emvai em ISO 8601 com fuso.2026-09-10T18:05:00-03:00, nunca2026-09-10 18:05. Sem fuso, a mesma leitura tem três horas de idade diferente conforme quem abre a página, e a conferência passa a depender de onde a pessoa está.- Data no futuro conta como vencida. Relógio errado, fuso esquecido ou dado montado à mão — nenhum dos três autoriza a afirmação, e aceitar futuro como "fresquíssimo" seria o jeito de burlar a conferência sem querer.
- Vencido, o preço sai de verdade — texto e atributo
value— e odata-dirsai junto. Não éopacity:0: essa deixaria o preço velho intacto na árvore de acessibilidade, e uma seta de alta sobre um traço vazio continuaria afirmando alta. - O selo "ao vivo" é concedido, não escrito. Ele só é exibido sob
[data-estado="vivo"]. Não adianta pô-lo no markup: sem leitura válida, o seletor não casa. Em card ele faz o papel que o movimento faz numa faixa — é a afirmação visual de que o número é de agora. - Card sem série fica sem minigráfico, nunca com uma linha reta. Linha plana desenhada por falta de dado é a mesma mentira do preço vencido, em forma de curva.
E o que fica por sua conta:
- Papel real pede preço real. Os papéis do markup e do preview são fictícios de propósito — MRD3, ALT4, NBY11, QDR3, das casas inventadas Meridiano, Altamira, Northbay e Quadrant. Papel inventado com preço inventado é demonstração; papel real com preço inventado é informação financeira falsa, e nenhum desenho de componente conserta isso.
- Cotação não é recomendação. Um painel de papéis ao lado de um botão "abra sua conta" pode ser lido como indicação de compra. Se a página vende serviço de investimento, escolha o que aparece com o jurídico e mantenha o painel longe do CTA.
- O atraso do provedor é informação, não letra miúda. Feed público de bolsa costuma vir com 15 minutos de defasagem, e o contrato de distribuição em geral exige que isso apareça. O slot
.coto-atrasoexiste para isso; deixe-o preenchido.
Quando usar
- Home ou página de mercado de casa de investimento, gestora ou corretora que tem feed de verdade.
- Página de research publicando um fechamento datado — aí é
data-validade="0": preços ficam, selo "ao vivo" nunca aparece. - Área logada leve ou painel interno, como resumo antes do detalhe.
- Dobra de "carteira recomendada", quando os papéis são poucos e nomeados.
Quando não usar
- Sem feed. Não existe versão decorativa deste componente. Um painel de preços que ninguém atualiza é o problema que ele foi construído para não ter.
- Para dar cara de mercado a um site que não é de mercado. Se o número não é o assunto da empresa, o painel é cenografia — e cenografia com preço é a pior espécie.
- Mais de oito papéis. Vira tabela, e tabela quer coluna, ordenação e busca. Aqui é vitrine de poucos.
- Série histórica. O minigráfico mostra a forma do dia, sem eixo e sem escala comum. Para comparar cenários,
dados/comparador-cenarios; para patrimônio,dados/painel-patrimonio.
O irmão mais próximo, e quando ele ganha
dados/grade-indicadores tem a mesma anatomia — 4 a 6 células, valor grande, variação com seta, data na peça — e por isso a escolha entre os dois merece uma linha:
grade-indicadores |
cards-cotacoes |
|
|---|---|---|
| O que exibe | o que já foi apurado | o que está sendo cotado agora |
| O tempo | a janela de apuração, impressa em texto | uma validade que expira e retira o valor |
| Script | nenhum, CSS puro | motor de validade e socket de feed |
| Sem alimentação | continua correto para sempre | entra em vencido, e isso é o certo |
A pergunta que decide: o número muda enquanto a página está aberta? Se não muda — patrimônio sob gestão, número de clientes, prazo médio —, é a grade, e ela ganha por ser CSS puro. Se muda, e ficar desatualizado é um problema, é este.
E há uma diferença de cor que segue dessa: na grade, direção não é veredito — o patrimônio sobe (boa notícia) e o prazo do diagnóstico cai (também boa notícia) —, então ela não usa --alta/--baixa. Cotação é das poucas peças em que subir já é lido como verde por convenção.
Padrão editorial
Quatro a seis papéis. Quatro fecham a fileira numa tela de 1240px sem sobra; acima de seis a segunda fileira começa a pedir hierarquia que o componente não tem.
O símbolo é o código; o nome é quem o leitor reconhece. MRD3 em cima, Meridiano Energia embaixo. Invertido, o card fica bonito e ilegível para quem não é do mercado.
O nome corta em uma linha, e isso é decisão de layout. Razão social longa ("Companhia Energética de …") empurraria o preço para baixo e desalinharia a fileira. Use o nome de pregão, não o do estatuto.
Casas decimais constantes por papel. Ação em duas, câmbio em quatro. O tabular-nums segura o dígito, não a quantidade deles.
Escolha por relevância para o leitor, não por variedade. Os papéis que a casa de fato acompanha — não seis nomes porque enchem melhor a fileira.
Como usar
<link rel="stylesheet" href="componentes/dados/cards-cotacoes/estilo.css">
<script src="componentes/dados/cards-cotacoes/script.js"></script>
Cole a seção, troque os papéis, e ligue o feed — sem feed o componente entra em vencido no primeiro carregamento depois do prazo, que é o comportamento correto e não um defeito.
Ganchos
| Gancho | O quê |
|---|---|
[data-coto] |
a raiz, na <section>. O script parte dela, e é nela que o evento de feed é disparado |
data-em |
ISO 8601 com fuso. O instante da leitura |
data-validade |
segundos de vida do dado. 900 = 15 min. 0 = fechamento datado |
[data-coto-grade] |
a <ul> dos cards. O feed reescreve o conteúdo dela |
data-dir |
sobe, cai ou zero no card. Resolve --coto-dir para traço, seta e percentual |
data-estado |
escrito pelo script: vivo, datado ou vencido. Não escreva à mão |
Ligando o feed
O feed entra por CustomEvent na raiz. Sem global, sem editar o HTML, sem tocar no script.js:
var raiz = document.querySelector('[data-coto]');
raiz.dispatchEvent(new CustomEvent('cotacoes:dados', { detail: {
em: '2026-09-10T18:05:00-03:00', // ISO com fuso; ausente, assume agora
provedor: 'B3',
atraso: 'atraso de 15 min',
itens: [
{ simbolo: 'PETR4', nome: 'Petrobras PN',
preco: 'R$ 38,42', precoNum: 38.42,
variacao: 0.0124,
serie: [36.9, 37.2, 37.1, 37.8, 38.4] },
{ simbolo: 'USDBRL', nome: 'Dólar comercial',
preco: 'R$ 5,1420', variacao: -0.0026 } // sem série: card sem gráfico
]
} }));
preco é a string que aparece — símbolo de moeda, separador e casas são decisão sua, e o componente não reformata. precoNum é opcional e vai para o atributo value do <data>, para quem lê a página por máquina.
variacao é número, e é fração — não porcento. 0.0124, não "+1,24%". Dela saem o sinal, a seta, a cor e o texto. Aceitar a string pronta deixaria a seta discordar do número no dia em que alguém montasse a string errada.
serie é opcional: um vetor de preços na ordem do dia. Sem ela, o card sai sem minigráfico.
Repita o dispatchEvent a cada leitura nova. Cada um renova o data-em e devolve o painel a vivo — inclusive saindo de vencido, se ele tinha expirado.
Parafusos
| Variável | Padrão | O quê |
|---|---|---|
--coto-min |
230px |
largura mínima do card antes de quebrar coluna |
--coto-gap |
14px |
vão entre cards |
--coto-pad |
18px |
respiro interno |
--coto-linha-h |
44px |
altura do minigráfico |
--coto-preco |
26px |
corpo do preço |
--coto-sobe |
var(--alta) |
cor da alta |
--coto-cai |
var(--baixa) |
cor da queda |
Variações
| Variação | Como |
|---|---|
| Outro par de estado | --coto-sobe e --coto-cai no bloco consumidor. O padrão já é o par de mercado, vindo de --alta/--baixa |
| Sem minigráfico | apagar o <svg class="coto-linha">. Vira o card do app de Ações em modo lista |
| Fechamento datado | data-validade="0". Preços ficam, selo "ao vivo" nunca aparece |
| No tom claro | trocar .sec-deep por .sec-paper. Nenhuma linha de CSS muda |
| Dois painéis | duas seções [data-coto], com validades diferentes. Cada uma vence sozinha |
Gotchas
-
preserveAspectRatio="none"exigevector-effect:non-scaling-stroke. O SVG do minigráfico estica até a largura do card, seja ela 230px ou 400px — é o que permite a mesma série servir a qualquer coluna. O preço é uma escala anisotrópica: sem ovector-effect, o traço engorda na horizontal e afina na vertical, e o gráfico do card largo fica visivelmente mais gordo que o do estreito. -
SVG não tem
z-index. A área preenchida entra antes da linha na ordem do documento; quem pinta por último fica por cima. Invertido, o preenchimento cobre metade do traço e o gráfico perde o contorno. -
A seta é triângulo de borda, não glifo.
▲e▼têm peso e altura diferentes em cada família tipográfica — num tema de fonte própria a seta de alta apareceria maior que a de queda, sem que ninguém tivesse pedido. O triângulo de borda é a mesma forma em qualquer fonte e herdacurrentColorsozinho. O casozeroprecisa zerar as bordas: semborder:0, o triângulo de alta continua desenhado por baixo do traço. -
--coto-diré a única fonte de direção, e é de propósito. Traço, área, seta e percentual leem a mesma variável, resolvida pelo[data-dir]do card — que o script escreve a partir do número da variação. É o que impede o gráfico de subir enquanto a seta aponta para baixo, que é o defeito clássico de painel montado por string. -
tabular-numsno preço não é estética. Sem ele, a troca de um dígito na atualização do feed muda a largura do preço e o bloco inteiro dança a cada leitura. Com ele, todo dígito ocupa a mesma caixa. -
aplicar()sai cedo quando o estado não muda. Um feed que chega com o painel já emvencidonão voltaria avivosem derrubar o estado antes — por issoreceber()remove odata-estadoe reconfere. Foi o primeiro bug do motor. -
setIntervalcongela em aba dormindo. Vários navegadores estrangulam o timer em aba de fundo, e uma aba aberta desde a manhã voltaria mostrando o estado de horas atrás por alguns segundos. Por isso a conferência também roda emvisibilitychange. -
A série é normalizada pelo próprio mínimo e máximo. Cada card mostra a forma do dia daquele papel, não a comparação entre papéis — que exigiria eixo comum, e eixo não cabe em 44px de altura. Consequência a saber: um papel que andou 0,1% e outro que andou 4% desenham curvas de amplitude parecida. Quem diz o tamanho do movimento é o percentual, não o gráfico.
-
Abaixo de 720px a grade vira trilho, não pilha. Quatro cards empilhados ocupam a tela inteira e empurram o resto da página; o trilho mostra dois e sugere o terceiro. É o que o app de Ações e o Google Finance fazem, e pela mesma razão. O trilho sangra até a borda com
margin-inlinenegativa e devolve o respiro porpadding-inline, porque a grade é filha do cabeçalho, que precisa continuar na coluna (§4).
Movimento reduzido / toque
O único movimento próprio é a pulsação do ponto do selo "ao vivo". Com prefers-reduced-motion: reduce o ponto fica e a pulsação sai — o ponto diz que a leitura está dentro do prazo, e isso é informação, não enfeite.
A entrada dos cards é o data-rv do core, já desligado por lá.
No toque não há hover, e nada se perde: o realce de borda é enfeite, e no celular a grade é um trilho com scroll-snap, arrastável com o dedo.
Histórico
| Origem | Quando | O quê |
|---|---|---|
| autoral | 2026-08 | mapeado na fila como "ticker de índices" e travado: cotação fictícia em site financeiro é compliance, não placeholder |
| autoral | 2026-09-10 | construído como faixa em esteira e reprovado na revisão pela forma |
| autoral | 2026-09-10 | refeito em cards, com o motor de validade inteiro aproveitado |
Por que demorou. O padrão estava na fila autoral desde a primeira leva, com a situação registrada como "previsto — compliance: só com feed real e data; valor fictício num site financeiro é problema, não placeholder". A fila estava certa em segurá-lo: um painel de cotação construído do jeito comum é um componente que mente por padrão, porque o estado natural de um feed é morrer sem avisar.
O que o destravou. Não foi decidir mostrar número falso com um aviso. Foi inverter a pergunta: em vez de "como avisar que o dado pode estar velho?", "o que faz este componente afirmar que o dado é novo?". A resposta virou estrutura — data-em, data-validade, a .coto-fonte obrigatória e um selo de liveness que só a conferência concede.
A forma mudou no caminho, e a primeira foi descartada. A construção original era uma faixa em esteira, no padrão de ticker de bolsa: símbolo, valor e variação correndo de lado, com a regra "movimento é uma afirmação de que o dado está vivo" — a esteira só andava no estado vivo. Funcionava e passava na conformidade. Foi reprovada na revisão pela forma, não pelo motor: em faixa a pessoa espera o papel dela passar; em card, ela procura. Numa página institucional, quem abre a dobra de mercado quer conferir um papel específico, e a faixa transforma isso em espera. O motor de validade migrou inteiro; o que se perdeu foi a esteira, e o selo "ao vivo" ocupou o lugar que o movimento tinha como afirmação de liveness.
Precedente de raciocínio. A prova/manchetes-imprensa, de seis dias antes, pôs <time datetime> e o href da matéria no markup como estrutura pela mesma razão. Os dois casos são o mesmo caso, e vale como método: quando a peça faz uma afirmação que não pode sustentar sozinha, o que falta vira estrutura do markup — não linha da ficha. Ficha ninguém lê na hora de colar; estrutura está no arquivo que a pessoa copia, e some junto se for apagada.
O verde e o vermelho custaram um token novo, e a conversa que o §3 exige. Este foi o componente que trouxe --alta e --baixa para o core/tokens.css. O §3 trata token global como decisão de arquitetura a discutir antes, e foi o que se fez — a alternativa, deixar as duas cores como parafuso local do componente, adiava o problema para o próximo que precisasse (o pill-status-pulsante está na fila do gabriel-rocha-studio esperando exatamente isso).
Duas decisões saíram junto, e as duas são do token, não deste componente:
- O nome é direção, não veredito — e isso custou uma correção no meio do caminho. Os tokens nasceram
--ok/--baixae foram renomeados antes de publicar, porque adados/grade-indicadoresjá tinha escrito o argumento um dia antes: na grade dela o patrimônio sobe (boa notícia) e o prazo do diagnóstico cai (também boa notícia). Um token chamado--okobrigaria a pintar de "bom" a metade errada dos casos. Também não são--verde/--vermelho: um tema troca o par por âmbar e azul sem nenhum componente mentir no próprio código. - E a
grade-indicadorescontinua fora deles, de propósito. Cotação é das poucas peças em que subir já é lido como verde por convenção; indicador de relatório não é. Os dois convivem, e o default dela segue em--accent/--accent-2. - Os valores são redefinidos por escopo de tom, e isso foi medido, não estimado: nenhum verde único alcança 4,5:1 contra
#0a0a0ae contra#ffffff. O par do escuro (#57bd82) cai para 2,3:1 sobre papel. Então.sec-dark/.sec-deeptêm um par e.sec-paper/.sec-lighttêm outro, como já acontece com--fge--rule.
E a regra que veio anexada ao token: cor nunca é o único portador da direção. O par verde/vermelho é exatamente o que 8% dos homens não distinguem. Aqui a direção está na seta, no sinal e por extenso em .visually-hidden — o componente continuaria legível em preto e branco.
Calibragem no primeiro uso. Espere ajustar --coto-min ao número de papéis: 230px dá quatro colunas em 1240px, e cinco papéis pedem 190px ou uma segunda fileira. Confira o data-validade contra a cadência real do seu feed — prazo mais curto que o intervalo de atualização faz o painel piscar entre vivo e vencido, que é pior que qualquer um dos dois. E meça o .coto-nome contra os seus nomes reais: o corte em uma linha é silencioso.