Card Grid

Grid responsivo de cards. Padrão do dashboard.

Abrir no Storybook ↗

Quando usar

  • Para exibir múltiplos cards do mesmo tipo — ou mix coerente — simultaneamente: dashboard financeiro, listagem curta de compromissos, grade de categorias de gasto.
  • Quando os itens têm o mesmo peso informacional e nenhum precisa de destaque hierárquico sobre os outros.
  • Como container de qualquer instância de Card: Commitment Card, Balance Card, Quick Actions Card encaixam sem modificação.

Quando não usar

  • Para exibir um único card — use o Card diretamente, sem wrapper de grid.
  • Para listagens longas (mais de 12 itens) — considerar virtualização e/ou listagem textual. Card Grid não pagina nem virtualiza; colocar 30 cards num grid é carga de render desnecessária.
  • Quando os itens têm alturas muito diferentes sem justificativa — a linha quebra o ritmo visual. Se as alturas variam muito, revisar o conteúdo ou usar layout alternativo.

Anatomia

Card Grid é container estrutural puro. Três propriedades definem o comportamento.

Propriedades do componente:

  • items — lista de Card (ou instâncias nomeadas). O Card Grid não injeta conteúdo — apenas organiza os filhos.
  • columns — número de colunas: auto-fit (responsivo, default), 3, 2 ou 1. Definido pelo modifier de classe no consumer.
  • gap — espaçamento entre itens. Sempre múltiplo de 8. Default: --space-24 (24px).

Variantes

Variante / ModificadorColunasUso
.peppe-card-grid (default)auto-fitminmax(280px, 1fr)Responsivo nativo. Cards quebram linha conforme a largura disponível. Adequado quando o consumidor não controla o breakpoint.
.peppe-card-grid--cols-33 colunas fixasDesktop — dashboard com três compromissos em linha. Não usar abaixo de 768px sem override do consumidor.
.peppe-card-grid--cols-22 colunas fixasTablet — cards mais densos, menos espaço lateral.
.peppe-card-grid--cols-11 colunaMobile — cards empilhados em coluna única. Ordem de leitura linear.

Responsividade é do consumidor, não do componente. Os modificadores --cols-* são escolhidos por quem consome o Card Grid. O default auto-fit já é naturalmente responsivo por minmax(280px, 1fr) — nenhum media query interno. Quando o consumidor precisar de breakpoints específicos, ele aplica o modifier correto via CSS de contexto ou classe condicional.

COMPROMISSOCondomínio

Vence em 5 dias. R$ 380.

COMPROMISSOInternet

Vence em 12 dias. R$ 110.

COMPROMISSOLuz

Vence em 18 dias. R$ 210.

COMPROMISSOAcademia

Vence em 22 dias. R$ 90.

auto-fit — 4 cards · wrap natural conforme largura
COMPROMISSOCondomínio

Vence em 5 dias. R$ 380.

COMPROMISSOInternet

Vence em 12 dias. R$ 110.

COMPROMISSOLuz

Vence em 18 dias. R$ 210.

cols-3 — 3 cards em linha · dashboard de compromissos
COMPROMISSOCondomínio

Vence em 5 dias. R$ 380.

COMPROMISSOInternet

Vence em 12 dias. R$ 110.

cols-2 — 2 colunas · tablet
COMPROMISSOCondomínio

Vence em 5 dias. R$ 380.

COMPROMISSOInternet

Vence em 12 dias. R$ 110.

cols-1 — empilhado · mobile

Variantes por canal. app: CSS Grid (default — responsivo com os modificadores acima). whatsapp: lista sequencial de blocos de mensagem, um por Card; o grid colapsa em enumeração vertical. voz: locução linear — cada Card vira um turno enumerado ("primeiro…", "segundo…", "terceiro…"). e-mail: tabela HTML fallback com uma linha por Card, CSS inline pra garantir render consistente. sms: lista numerada ("1. […] 2. […] 3. […]") com keyword de ação se o Card tiver footer.

Estados

COMPROMISSOCondomínio

Vence em 5 dias. R$ 380.

COMPROMISSOInternet

Vence em 12 dias. R$ 110.

COMPROMISSOLuz

Vence em 18 dias. R$ 210.

default
loading — N skeletons preservando dimensões
Nenhum item aqui ainda. Manda um pra eu anotar.
vazio — EmptyState (texto entregue pelo host)

Sobre o EmptyState. O template canônico vive em .claude/product-design/components.md §6.7 — aqui ele é renderizado autônomo (fora de um Card), não como Card vazio: um Card Grid sem items por definição não tem Card-pai, então o EmptyState ocupa a área que os Cards ocupariam. O CTA usa peppe-button--secondary em vez do primary prescrito no §6.7 — débito conhecido enquanto peppe-button--primary (Tecla V-B) não tem classe de produto própria (ver D.3.1). Quando o Button primary virar classe consumível, o CTA do EmptyState migra.

Tokens aplicáveis

TokenValorPapel
--space-2424pxGap default entre cards — múltiplo de 8
--space-1616pxGap compacto — variação em container estreito (mobile) e gap interno do EmptyState entre ícone, texto e CTA
--color-ink-tertiary#A1A1A1Texto do EmptyState e label de estado
--color-ink-secondary#6B6B6BTexto do EmptyState (corpo)
--icon-size-md24pxÍcone do EmptyState
--type-family-sansInstrument SansTexto do EmptyState
--type-size-sm14pxTexto do EmptyState
--type-weight-regular400Peso do texto do EmptyState

Nota sobre padding externo. O Card Grid não declara padding próprio — o espaçamento externo ao grid é responsabilidade do container pai (seção, main, peppe-card__body). Isso mantém o componente flexível e evita conflito com o --space-* do container.

Conteúdo

Não aplicável — Card Grid é container estrutural e não tem conteúdo próprio. Todo o conteúdo é gerido pelos Card filhos. Ver Card §7 para as regras de conteúdo de cada slot.

Acessibilidade

  • Semântica de lista: o container usa <ul> com role="list" implícito; cada item usa <li> com role="listitem". Leitores de tela anunciam o número total de itens antes de entrar na navegação.
  • Navegação por Tab: Tab move o foco entre os itens interativos dentro de cada Card. O grid em si não é focável — o foco vai direto para o conteúdo interno.
  • Estado loading: o container <ul> recebe aria-busy="true". Cada skeleton filho recebe aria-hidden="true" — o anúncio único do container é suficiente; repetir "Carregando" N vezes gera ruído de leitura.
  • Estado vazio: o EmptyState usa role="status" para que leitores de tela o anunciem sem urgência. Texto curto e propositivo — sem desculpa, sem lamento.
  • Canais narrativos (voz, WhatsApp): em canais não-visuais o Card Grid vira enumeração linear — o SDUI resolve o colapso antes da entrega ao canal.

Do / Don't

Do

Item A

Item B

Gap em múltiplo de 8 — --space-24 (24px). Ritmo de grade consistente com o sistema de espaçamento.

Don't

Item A

Item B

Gap arbitrário — 10px ou 15px fora da escala de 8. O espaçamento quebra o ritmo do sistema e acumula desalinhamento.

Do

Cards da mesma linha com altura uniforme — conteúdo equivalente, mesmos slots preenchidos. O grid lê como linha limpa; os olhos deslizam horizontalmente sem tropeçar.

Don't

Cards com alturas muito diferentes na mesma linha sem justificativa — um card tem título + body + footer; o vizinho tem só um título. O ritmo da grade quebra; o espaço negativo embaixo do card menor vira ruído visual.

Componentes relacionados

  • Card — o item que o Card Grid organiza. Card Grid sem Card não tem sentido.
  • Balance Card — instância nomeada de Card. Encaixa como item do Card Grid quando há mais de um saldo em tela.
  • Commitment Card — instância nomeada de Card. O padrão de uso mais comum do Card Grid: três compromissos em linha em dashboard desktop.
  • Quick Actions Card — instância nomeada de Card. Pode coexistir com Commitment Cards dentro do mesmo Card Grid.

Markup de referência

HTML namespaced copiável para as quatro variantes de coluna, estado loading e estado vazio.

<!-- Card Grid auto-fit (default — responsivo) --> <ul className="peppe-card-grid" role="list"> <li className="peppe-card-grid__item" role="listitem"> <article className="peppe-card" aria-label="Condomínio — R$ 380 — vence em 5 dias"> <header className="peppe-card__header"> <span className="peppe-card__kicker">COMPROMISSO</span> <h3 className="peppe-card__title">Condomínio</h3> </header> <div className="peppe-card__body"> <p>Vence em 5 dias. R$ 380.</p> </div> </article> </li> <!-- repetir <li> para cada card --> </ul> <!-- Card Grid 3 colunas (desktop — dashboard de compromissos) --> <ul className="peppe-card-grid peppe-card-grid--cols-3" role="list"> <li className="peppe-card-grid__item" role="listitem"> <article className="peppe-card">...</article> </li> <li className="peppe-card-grid__item" role="listitem"> <article className="peppe-card">...</article> </li> <li className="peppe-card-grid__item" role="listitem"> <article className="peppe-card">...</article> </li> </ul> <!-- Card Grid 2 colunas (tablet) --> <ul className="peppe-card-grid peppe-card-grid--cols-2" role="list"> <li className="peppe-card-grid__item" role="listitem"> <article className="peppe-card">...</article> </li> <li className="peppe-card-grid__item" role="listitem"> <article className="peppe-card">...</article> </li> </ul> <!-- Card Grid 1 coluna (mobile) --> <ul className="peppe-card-grid peppe-card-grid--cols-1" role="list"> <li className="peppe-card-grid__item" role="listitem"> <article className="peppe-card">...</article> </li> <li className="peppe-card-grid__item" role="listitem"> <article className="peppe-card">...</article> </li> </ul> <!-- Estado loading (N skeletons — preservam dimensões) --> <ul className="peppe-card-grid peppe-card-grid--cols-3" role="list" aria-busy="true"> <li className="peppe-card-grid__item" role="listitem"> <article className="peppe-card peppe-card--loading" aria-busy="true" aria-label="Carregando"> <div className="peppe-card__skeleton-header"> <div className="peppe-card__skeleton-line peppe-card__skeleton-line--xs"></div> <div className="peppe-card__skeleton-line peppe-card__skeleton-line--md"></div> </div> <div className="peppe-card__skeleton-body"> <div className="peppe-card__skeleton-line peppe-card__skeleton-line--lg"></div> <div className="peppe-card__skeleton-line peppe-card__skeleton-line--sm"></div> </div> </article> </li> <!-- repetir <li> para cada skeleton --> </ul> <!-- Estado vazio (EmptyState) --> <div className="peppe-card-grid__empty-state" role="status" aria-label="Nenhum item encontrado"> <!-- ícone neutro (rect + linhas) --> <p className="peppe-card-grid__empty-text"> Nenhum item aqui ainda. Manda um pra eu anotar. </p> <button className="peppe-button peppe-button--secondary" type="button"> <span className="peppe-button__label">Adicionar</span> </button> </div>