Por que listas e cards são padrões tão comuns
Em apps, a maior parte do conteúdo aparece como coleções: mensagens, produtos, tarefas, notícias. No Ionic, o padrão mais direto para isso é combinar ion-list + ion-item para listagens e ion-card para destacar conteúdo em blocos. Além do visual, esses componentes já trazem comportamento mobile esperado: áreas de toque adequadas, alinhamento consistente, suporte a ícones, thumbnails e acessibilidade.
Listagem com ion-list e ion-item
Estrutura básica
Uma lista típica usa ion-list como contêiner e vários ion-item como linhas. Dentro do item, você pode usar ion-label (texto), ion-icon (ícone), ion-note (informação secundária) e ion-thumbnail (imagem).
<ion-list> <ion-item> <ion-label> <h2>Título</h2> <p>Descrição curta</p> </ion-label> </ion-item></ion-list>Lista com ícone, texto secundário e “chevron” de navegação
Para indicar que o item leva a outra tela, use detail="true" (mostra o indicador de navegação no iOS e mantém consistência visual).
<ion-list> <ion-item detail="true"> <ion-icon name="document-text-outline" slot="start"></ion-icon> <ion-label> <h2>Relatório mensal</h2> <p>Atualizado há 2 dias</p> </ion-label> <ion-note slot="end">PDF</ion-note> </ion-item></ion-list>Lista com avatar/thumbnail
<ion-list> <ion-item> <ion-thumbnail slot="start"> <img alt="Foto do contato" src="assets/avatar1.jpg" /> </ion-thumbnail> <ion-label> <h2>Ana Souza</h2> <p>Online</p> </ion-label> </ion-item></ion-list>Cards com ion-card: quando usar e como compor
ion-card é ideal quando cada item precisa de mais espaço visual, hierarquia de informação (título, subtítulo, imagem, ações) ou quando você quer “blocos” que funcionem bem em feeds e dashboards.
Card básico com header e conteúdo
<ion-card> <ion-card-header> <ion-card-title>Promoção do dia</ion-card-title> <ion-card-subtitle>Somente hoje</ion-card-subtitle> </ion-card-header> <ion-card-content> Desconto de 20% em itens selecionados. </ion-card-content></ion-card>Card com imagem e ações
Use ion-button dentro do card para ações rápidas. Em mobile, é comum colocar ações no rodapé do card.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
<ion-card> <img alt="Foto do produto" src="assets/product.jpg" /> <ion-card-header> <ion-card-title>Fone Bluetooth</ion-card-title> <ion-card-subtitle>R$ 199,90</ion-card-subtitle> </ion-card-header> <ion-card-content> <ion-button fill="solid" size="small">Comprar</ion-button> <ion-button fill="clear" size="small">Detalhes</ion-button> </ion-card-content></ion-card>Estados de tela: carregando, vazio e erro
Uma tela de listagem raramente está sempre “pronta”. Você precisa prever: carregamento (buscando dados), vazio (sem resultados) e, muitas vezes, erro (falha na requisição). Implementar esses estados evita telas “em branco” e melhora a percepção de qualidade.
Modelo de estado com variáveis
Uma abordagem simples é controlar três flags: loading, error e a lista (items). A UI decide o que renderizar com *ngIf.
// exemplo de estado no componente (TypeScript)loading = true;error: string | null = null;items: Array<{ id: string; title: string; subtitle?: string }> = [];Exibindo skeleton durante carregamento
ion-skeleton-text simula o layout do conteúdo enquanto os dados chegam, reduzindo a sensação de espera.
<ng-container *ngIf="loading"> <ion-list> <ion-item *ngFor="let i of [1,2,3,4,5]"> <ion-label> <h2><ion-skeleton-text animated style="width: 60%"></ion-skeleton-text></h2> <p><ion-skeleton-text animated style="width: 40%"></ion-skeleton-text></p> </ion-label> </ion-item> </ion-list></ng-container>Estado vazio com mensagem e ação
Quando não há itens, mostre uma mensagem clara e uma ação possível (ex.: “Atualizar”, “Criar novo”, “Limpar filtros”).
<ng-container *ngIf="!loading && !error && items.length === 0"> <ion-card> <ion-card-header> <ion-card-title>Nada por aqui</ion-card-title> </ion-card-header> <ion-card-content> Nenhum item encontrado. Tente atualizar ou criar um novo. <div style="margin-top: 12px;"> <ion-button>Atualizar</ion-button> <ion-button fill="outline">Criar</ion-button> </div> </ion-card-content> </ion-card></ng-container>Estado de erro com retry
<ng-container *ngIf="!loading && error"> <ion-card color="light"> <ion-card-header> <ion-card-title>Falha ao carregar</ion-card-title> </ion-card-header> <ion-card-content> <p>{{ error }}</p> <ion-button>Tentar novamente</ion-button> </ion-card-content> </ion-card></ng-container>Interação: clique, seleção e ações por item
Click no item (abrir detalhes ou executar ação)
Para capturar toque/clique, use (click) no ion-item. Mantenha o item com aparência “clicável” usando button="true".
<ion-list> <ion-item button="true" (click)="openItem(item)" *ngFor="let item of items"> <ion-label> <h2>{{ item.title }}</h2> <p *ngIf="item.subtitle">{{ item.subtitle }}</p> </ion-label> </ion-item></ion-list>// TypeScriptopenItem(item: { id: string }) { // aqui você pode navegar, abrir modal, etc. console.log('Abrir item', item.id);}Seleção (single e múltipla) com ion-checkbox
Para seleção múltipla, coloque um ion-checkbox no slot="start" e controle o estado no componente. Evite depender apenas do clique no item para alternar seleção quando houver outras ações no mesmo item.
<ion-list> <ion-item *ngFor="let item of items"> <ion-checkbox slot="start" [checked]="selectedIds.has(item.id)" (ionChange)="toggleSelection(item.id, $event.detail.checked)"> </ion-checkbox> <ion-label>{{ item.title }}</ion-label> </ion-item></ion-list>// TypeScriptselectedIds = new Set<string>();toggleSelection(id: string, checked: boolean) { if (checked) this.selectedIds.add(id); else this.selectedIds.delete(id);}Ações rápidas com ion-buttons no item
Quando você precisa de ações por linha (ex.: favoritar, editar), use botões no slot="end". Um cuidado importante: se o ion-item também for clicável, o clique no botão pode “propagar” e disparar o clique do item. Use $event.stopPropagation() no botão.
<ion-item button="true" (click)="openItem(item)" *ngFor="let item of items"> <ion-label>{{ item.title }}</ion-label> <ion-buttons slot="end"> <ion-button fill="clear" (click)="toggleFavorite(item, $event)"> <ion-icon name="star-outline"></ion-icon> </ion-button> <ion-button fill="clear" color="danger" (click)="remove(item, $event)"> <ion-icon name="trash-outline"></ion-icon> </ion-button> </ion-buttons></ion-item>// TypeScripttoggleFavorite(item: any, ev: Event) { ev.stopPropagation(); item.favorite = !item.favorite;}remove(item: any, ev: Event) { ev.stopPropagation(); console.log('Remover', item.id);}Swipe actions com ion-item-sliding
Para ações de “arrastar para o lado” (muito comum em listas), use ion-item-sliding com ion-item-options. Isso mantém o padrão de interação esperado em apps mobile.
<ion-list> <ion-item-sliding *ngFor="let item of items"> <ion-item button="true" (click)="openItem(item)"> <ion-label>{{ item.title }}</ion-label> </ion-item> <ion-item-options side="end"> <ion-item-option color="primary" (click)="edit(item)">Editar</ion-item-option> <ion-item-option color="danger" (click)="remove(item)">Excluir</ion-item-option> </ion-item-options> </ion-item-sliding></ion-list>Passo a passo prático: tela de “Artigos” com lista, card e estados
1) Defina o modelo de dados e o estado
// TypeScript (exemplo)type Article = { id: string; title: string; excerpt: string; category: string; featured?: boolean;};loading = true;error: string | null = null;articles: Article[] = [];featured: Article | null = null;2) Simule o carregamento e preencha os dados
Em um app real, você buscaria dados de um serviço. Aqui, o foco é a UI: carregue, trate erro e separe um item em destaque para o card.
// TypeScript (exemplo)async loadArticles() { this.loading = true; this.error = null; try { await new Promise(r => setTimeout(r, 800)); this.articles = [ { id: '1', title: 'Ionic Components na prática', excerpt: 'Como montar telas com consistência...', category: 'UI', featured: true }, { id: '2', title: 'Listas eficientes', excerpt: 'Padrões para exibir coleções...', category: 'Padrões' }, { id: '3', title: 'Interações comuns', excerpt: 'Clique, swipe e seleção...', category: 'UX' } ]; this.featured = this.articles.find(a => a.featured) ?? null; } catch (e) { this.error = 'Não foi possível carregar os artigos.'; } finally { this.loading = false; }}3) Monte o template com prioridades de renderização
Ordem recomendada: loading → erro → vazio → conteúdo. Isso evita condições conflitantes e facilita manutenção.
<!-- Loading --><ng-container *ngIf="loading"> <ion-card> <ion-card-header> <ion-card-title><ion-skeleton-text animated style="width: 50%"></ion-skeleton-text></ion-card-title> </ion-card-header> <ion-card-content> <ion-skeleton-text animated style="width: 90%"></ion-skeleton-text> <ion-skeleton-text animated style="width: 80%"></ion-skeleton-text> </ion-card-content> </ion-card> <ion-list> <ion-item *ngFor="let i of [1,2,3,4]"> <ion-label> <h2><ion-skeleton-text animated style="width: 60%"></ion-skeleton-text></h2> <p><ion-skeleton-text animated style="width: 40%"></ion-skeleton-text></p> </ion-label> </ion-item> </ion-list></ng-container><!-- Error --><ng-container *ngIf="!loading && error"> <ion-card> <ion-card-header> <ion-card-title>Erro</ion-card-title> </ion-card-header> <ion-card-content> <p>{{ error }}</p> <ion-button (click)="loadArticles()">Tentar novamente</ion-button> </ion-card-content> </ion-card></ng-container><!-- Empty --><ng-container *ngIf="!loading && !error && articles.length === 0"> <ion-card> <ion-card-header> <ion-card-title>Sem artigos</ion-card-title> </ion-card-header> <ion-card-content> <p>Ainda não há conteúdo disponível.</p> <ion-button fill="outline" (click)="loadArticles()">Atualizar</ion-button> </ion-card-content> </ion-card></ng-container><!-- Content --><ng-container *ngIf="!loading && !error && articles.length > 0"> <ion-card *ngIf="featured"> <ion-card-header> <ion-card-subtitle>Destaque</ion-card-subtitle> <ion-card-title>{{ featured.title }}</ion-card-title> </ion-card-header> <ion-card-content> <p>{{ featured.excerpt }}</p> <ion-button size="small" (click)="openArticle(featured)">Ler</ion-button> </ion-card-content> </ion-card> <ion-list> <ion-item-sliding *ngFor="let a of articles"> <ion-item button="true" detail="true" (click)="openArticle(a)"> <ion-label> <h2>{{ a.title }}</h2> <p>{{ a.excerpt }}</p> </ion-label> <ion-note slot="end">{{ a.category }}</ion-note> </ion-item> <ion-item-options side="end"> <ion-item-option color="primary" (click)="share(a)">Compartilhar</ion-item-option> <ion-item-option color="danger" (click)="archive(a)">Arquivar</ion-item-option> </ion-item-options> </ion-item-sliding> </ion-list></ng-container>4) Implemente as ações
// TypeScriptexport class ArticlesPage { // ...estado e loadArticles() openArticle(a: { id: string }) { console.log('Abrir artigo', a.id); } share(a: { id: string }) { console.log('Compartilhar', a.id); } archive(a: { id: string }) { console.log('Arquivar', a.id); }}Reutilização: estruturando componentes para listas e cards
Quando você repete o mesmo padrão de item em várias telas (ex.: “linha de produto”, “linha de contato”), crie um componente dedicado. Isso melhora consistência visual, reduz bugs e facilita evoluções (por exemplo, adicionar um ícone ou alterar espaçamentos em um único lugar).
Exemplo: componente reutilizável de item de lista
Ideia: um componente <app-article-list-item> que recebe um artigo e emite eventos para o pai. Assim, o pai decide o que fazer (navegar, abrir modal, etc.), e o componente só cuida do layout e da interação local.
| Responsabilidade | Fica onde? |
|---|---|
| Layout do item (label, note, ícone) | Componente reutilizável |
| Regras de navegação e fluxo | Página (container) |
| Ações (editar, excluir) e permissões | Página/serviços |
<!-- Uso na página --><ion-list> <app-article-list-item *ngFor="let a of articles" [article]="a" (open)="openArticle($event)" (archive)="archive($event)"> </app-article-list-item></ion-list>// API do componente (TypeScript - exemplo)@Input() article!: { id: string; title: string; excerpt: string; category: string };@Output() open = new EventEmitter<any>();@Output() archive = new EventEmitter<any>();<!-- Template do componente --><ion-item-sliding> <ion-item button="true" detail="true" (click)="open.emit(article)"> <ion-label> <h2>{{ article.title }}</h2> <p>{{ article.excerpt }}</p> </ion-label> <ion-note slot="end">{{ article.category }}</ion-note> </ion-item> <ion-item-options side="end"> <ion-item-option color="danger" (click)="archive.emit(article)">Arquivar</ion-item-option> </ion-item-options></ion-item-sliding>Consistência visual e de comportamento em fluxos
Padrões práticos para manter consistência
- Área clicável previsível: se o item abre detalhes, use
button="true"edetail="true"; se não abre, evite “parecer clicável”. - Ações no mesmo lugar: mantenha ações secundárias no
slot="end"(botões) ou em swipe (ion-item-sliding) de forma consistente entre telas. - Estados padronizados: use o mesmo padrão de skeleton, card de vazio e card de erro em todas as listagens. Isso reduz esforço cognitivo do usuário.
- Hierarquia de informação: título em
<h2>, descrição em<p>, metadados emion-note. Evite misturar formatos em telas diferentes. - Componentização: crie componentes para “linha de lista” e “card de destaque” quando o padrão se repetir. A página fica responsável por dados e fluxo; o componente, por UI.
Checklist rápido de UX para listas no Ionic
- O usuário entende o que é clicável?
- Existe feedback de carregamento (skeleton/spinner) antes do conteúdo?
- Existe estado vazio com orientação e ação?
- As ações por item não conflitam com o clique principal (use
stopPropagationquando necessário)? - O mesmo padrão de item se repete em outras telas? Se sim, transforme em componente reutilizável.