Listas, cards e interação: padrões de UI para conteúdo no Ionic

Capítulo 8

Tempo estimado de leitura: 14 minutos

+ Exercício

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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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: loadingerrovazioconteú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.

ResponsabilidadeFica onde?
Layout do item (label, note, ícone)Componente reutilizável
Regras de navegação e fluxoPágina (container)
Ações (editar, excluir) e permissõesPá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" e detail="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 em ion-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 stopPropagation quando necessário)?
  • O mesmo padrão de item se repete em outras telas? Se sim, transforme em componente reutilizável.

Agora responda o exercício sobre o conteúdo:

Em uma tela de listagem no Ionic, qual prática ajuda a evitar conflitos entre estados (carregando, erro, vazio e conteúdo) e facilita a manutenção do template?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Ao priorizar loading → erro → vazio → conteúdo e controlar com flags (ex.: loading, error, lista), você evita condições conflitantes e mantém o template mais previsível e fácil de manter.

Próximo capitúlo

Formulários no Ionic: inputs, validação e UX mobile

Arrow Right Icon
Capa do Ebook gratuito Ionic para Iniciantes: aplicativos híbridos com HTML, CSS e TypeScript
38%

Ionic para Iniciantes: aplicativos híbridos com HTML, CSS e TypeScript

Novo curso

21 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.