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 Cardencaixam sem modificação.
Quando não usar
- Para exibir um único card — use o
Carddiretamente, 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,2ou1. Definido pelo modifier de classe no consumer. - gap — espaçamento entre itens. Sempre múltiplo de 8. Default:
--space-24(24px).
Variantes
| Variante / Modificador | Colunas | Uso |
|---|---|---|
.peppe-card-grid (default) | auto-fit — minmax(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-3 | 3 colunas fixas | Desktop — dashboard com três compromissos em linha. Não usar abaixo de 768px sem override do consumidor. |
.peppe-card-grid--cols-2 | 2 colunas fixas | Tablet — cards mais densos, menos espaço lateral. |
.peppe-card-grid--cols-1 | 1 coluna | Mobile — 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.
Vence em 5 dias. R$ 380.
Vence em 12 dias. R$ 110.
Vence em 18 dias. R$ 210.
Vence em 22 dias. R$ 90.
Vence em 5 dias. R$ 380.
Vence em 12 dias. R$ 110.
Vence em 18 dias. R$ 210.
Vence em 5 dias. R$ 380.
Vence em 12 dias. R$ 110.
Vence em 5 dias. R$ 380.
Vence em 12 dias. R$ 110.
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
Vence em 5 dias. R$ 380.
Vence em 12 dias. R$ 110.
Vence em 18 dias. R$ 210.
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
| Token | Valor | Papel |
|---|---|---|
--space-24 | 24px | Gap default entre cards — múltiplo de 8 |
--space-16 | 16px | Gap compacto — variação em container estreito (mobile) e gap interno do EmptyState entre ícone, texto e CTA |
--color-ink-tertiary | #A1A1A1 | Texto do EmptyState e label de estado |
--color-ink-secondary | #6B6B6B | Texto do EmptyState (corpo) |
--icon-size-md | 24px | Ícone do EmptyState |
--type-family-sans | Instrument Sans | Texto do EmptyState |
--type-size-sm | 14px | Texto do EmptyState |
--type-weight-regular | 400 | Peso 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>comrole="list"implícito; cada item usa<li>comrole="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>recebearia-busy="true". Cada skeleton filho recebearia-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>