Vue.js: Slots

Última atualização: 2026-08-26

Os slots fazem parte da API de Distribuição de Conteúdo do Vue — os componentes pais podem inserir qualquer conteúdo nos “slots” de um componente filho. Os slots tornam os componentes tão flexíveis quanto os “modelos”: os componentes pais controlam o que é exibido, enquanto os componentes filhos controlam onde isso é exibido.

O Vue 3 oferece três tipos de slots: slots padrão, slots nomeados e slots com escopo. Esta lição vai ajudá-lo a dominar o uso e as melhores práticas para esses três tipos.

1. O que você vai aprender


2. O desafio de não poder personalizar o conteúdo de um componente de cartão

(1) Desafio: 5 estilos de cartão, exigindo a criação de 5 componentes

O administrador do site de comércio eletrônico da Alice precisava de 5 tipos de cartões:

VUE
<!-- ❌ The "Broken" Version:5 component -->
<!-- ProductCard.vue -->
<div class="card">{{ product.name }}</div>

<!-- UserCard.vue -->
<div class="card">{{ user.name }}</div>

<!-- OrderCard.vue -->
<div class="card">Order #{{ order.id }}</div>

<!-- 5 component,90% The code is the same,The only difference is the content -->

O gerente de produto Charlie:

“Alice, preciso de mais 5 tipos de cartão na próxima semana. Não podemos ficar adicionando novos componentes. Precisamos de um cartão flexível que aceite qualquer tipo de conteúdo.”

(2) Solução com slots do Vue: 1 BaseCard, com conteúdo fornecido pelo componente pai

VUE
<!-- BaseCard.vue - General-Purpose Card,The slot accepts any content -->
<template>
  <div class="card">
    <div v-if="$slots.header" class="card-header">
      <slot name="header" />
    </div>
    <div class="card-body">
      <slot />  <!-- Default Slot -->
    </div>
    <div v-if="$slots.footer" class="card-footer">
      <slot name="footer" />
    </div>
  </div>
</template>
VUE
<!-- ProductCard Usage BaseCard -->
<BaseCard>
  <template #header>
    <h3>Product</h3>
  </template>
  
  <p>iPhone 15 Pro</p>  <!-- Default Slot -->
  
  <template #footer>
    <button>Add to Cart</button>
  </template>
</BaseCard>

1 BaseCard.vue → uma variedade infinita de cartões. Sempre que você adicionar um novo, basta escrever o conteúdo — não é preciso modificar o componente.

(3) Receita

Após usar os slots:


3. Slot padrão

(1) Uso básico

VUE
<!-- BaseCard.vue Child component -->
<template>
  <div class="card">
    <!-- Slot: The parent component can contain any content -->
    <slot />
  </div>
</template>
VUE
<!-- App.vue Parent Component -->
<template>
  <BaseCard>
    <h3>Hello</h3>
    <p>This is the card content</p>
  </BaseCard>
</template>

Resultados da renderização:

HTML
<div class="card">
  <h3>Hello</h3>
  <p>This is the card content</p>
</div>

(2) Conteúdo padrão (alternativo)

VUE
<!-- Child component -->
<template>
  <div class="card">
    <slot>
      <!-- Default Content:Display when the parent component does not pass data -->
      <p>No content provided</p>
    </slot>
  </div>
</template>
VUE
<!-- The parent component does not pass content -->
<BaseCard />

<!-- Rendering:<p>No content provided</p> -->

<!-- Passing Content from the Parent Component -->
<BaseCard>
  <p>Custom content</p>
</BaseCard>

<!-- Rendering:<p>Custom content</p>(Override Default)-->

(3) 5 principais casos de uso

Cenário Uso
Conteúdo do cartão <slot />
Texto do botão <slot />
Item da lista <slot :item="item" />
Campo do formulário <slot />
Corpo da caixa modal <slot />

4. Slots nomeados

(1) Definição de slots nomeados

VUE
<!-- BaseLayout.vue Child component -->
<template>
  <div class="layout">
    <header>
      <slot name="header" />
    </header>
    <main>
      <slot />  <!-- Default Slot(Optional name="default") -->
    </main>
    <footer>
      <slot name="footer" />
    </footer>
  </div>
</template>

(2) Como usar componentes pai (3 sintaxes)

VUE
<!-- App.vue Parent Component -->

<!-- Writing Style 1:v-slot:name(Most Comprehensive) -->
<BaseLayout>
  <template v-slot:header>
    <h1>My App</h1>
  </template>
  
  <template v-slot:default>
    <p>Main content</p>
  </template>
  
  <template v-slot:footer>
    <p>Footer</p>
  </template>
</BaseLayout>

<!-- Writing Style 2:Abbreviation #name(Recommendations) -->
<BaseLayout>
  <template #header>
    <h1>My App</h1>
  </template>
  
  <p>Main content</p>  <!-- The default slot can be omitted template -->
  
  <template #footer>
    <p>Footer</p>
  </template>
</BaseLayout>

<!-- Writing Style 3:v-slot Receive multiple slot objects(Vue 3 Recommendations) -->
<BaseLayout>
  <template #header>
    <h1>My App</h1>
  </template>
  
  <template #default="{ user }">  <!-- Deconstructing Scope Slots -->
    <p>Hello, {{ user.name }}</p>
  </template>
  
  <template #footer>
    <button>Logout</button>
  </template>
</BaseLayout>

(3) Nomes dinâmicos de slots

VUE
<!-- Child component -->
<template>
  <div>
    <slot :name="dynamicSlotName" />
  </div>
</template>

<script setup>
import { ref } from 'vue'
const dynamicSlotName = ref('header')
</script>

<!-- Parent Component:Dynamic Slot Binding -->
<BaseLayout>
  <template #[dynamicSlotName]>
    <p>Dynamic content</p>
  </template>
</BaseLayout>

(4) Verifique se há um encaixe

VUE
<!-- Child component:Check whether data is being received through the slot -->
<template>
  <div class="card">
    <div v-if="$slots.header" class="card-header">
      <slot name="header" />
    </div>
    <slot />
  </div>
</template>
VUE
<!-- Parent Component -->
<BaseCard>
  <template #header>...</template>  <!-- Sent header Slot, Display -->
</BaseCard>

<BaseCard>
  <!-- No header slot passed, Do not display card-header -->
</BaseCard>

5. Espaços para miras telescópicas

(1) Ponto-chave: os componentes filhos passam dados para os slots do componente pai

VUE
<!-- Child component:UserList.vue -->
<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      <!-- Pass user to the parent component's slot -->
      <slot :user="user" :index="index" />
    </li>
  </ul>
</template>

<script setup>
defineProps({ users: Array })
</script>
VUE
<!-- Parent Component:App.vue -->
<template>
  <UserList :users="users">
    <!-- Receive data passed from child components -->
    <template #default="{ user, index }">
      <p>{{ index + 1 }}. {{ user.name }} ({{ user.email }})</p>
    </template>
  </UserList>
</template>

Resultados da renderização:

HTML
<ul>
  <li><p>1. Alice (alice@example.com)</p></li>
  <li><p>2. Bob (bob@example.com)</p></li>
  <li><p>3. Charlie (charlie@example.com)</p></li>
</ul>

(2) 5 Padrões de encaixe para miras telescópicas

Padrão Implementação do componente pai Finalidade
Aceita todos os props <template #default="slotProps"> Aceita o objeto inteiro
Desconstrução <template #default="{ user }"> Use apenas os campos de que você precisa
Renomear <template #default="{ user: u }"> Evitar conflitos de variáveis
Valor padrão <template #default="{ user = defaultUser }"> Forneça um valor padrão durante a desconstrução
Sem modelo {{ slotProps.user.name }} Cenário simples (espaço padrão)

(3) Exemplo completo: Lista personalizável

VUE
<!-- UserList.vue Child component -->
<template>
  <ul class="user-list">
    <li v-for="(user, index) in users" :key="user.id">
      <slot :user="user" :index="index" :isAdmin="user.role === 'admin'" />
    </li>
  </ul>
</template>

<script setup>
defineProps({ users: Array })
</script>
▶ Experimente
VUE
<!-- Using Parent Components(3 One way) -->
<template>
  <!-- Method 1:A Brief Overview -->
  <UserList :users="users">
    <template #default="{ user }">
      {{ user.name }}
    </template>
  </UserList>
  
  <!-- Method 2:Table Display -->
  <UserList :users="users">
    <template #default="{ user, index, isAdmin }">
      <tr>
        <td>{{ index + 1 }}</td>
        <td>{{ user.name }}</td>
        <td>{{ user.email }}</td>
        <td>
          <span v-if="isAdmin" class="badge admin">Admin</span>
          <span v-else class="badge user">User</span>
        </td>
      </tr>
    </template>
  </UserList>
  
  <!-- Method 3:Card Display -->
  <UserList :users="users">
    <template #default="{ user }">
      <UserCard :user="user" />
    </template>
  </UserList>
</template>

6. $slots e useSlots

(1) $slots Acesso aos slots

VUE
<!-- Child component -->
<template>
  <div>
    <!-- List all incoming slot names -->
    <div v-for="(_, name) in $slots" :key="name">
      <slot :name="name" />
    </div>
  </div>
</template>

(2) useSlots (API de Composição)

VUE
<script setup>
import { useSlots } from 'vue'

const slots = useSlots()

// Check if the slot exists
if (slots.header) {
  console.log('Header slot exists')
}

// Dynamic Rendering
if (slots.default) {
  console.log('Default slot exists')
}
</script>

(3) $slots x useSlots

Dimensão $slots useSlots()
Tipo de API Usada diretamente nos modelos Usada em JS (API de Composição)
Voltar Objeto Slot Objeto Slot (formato refs)
Cena Dentro do modelo Dentro de <script setup>

7. Exemplo completo: Componente de cartão personalizável

▶ Exemplo: 1. BaseCard.vue (padrão + slots nomeados)

VUE
<!-- src/components/BaseCard.vue -->
<template>
  <div class="card">
    <div v-if="$slots.header" class="card-header">
      <slot name="header" />
    </div>
    <div class="card-body">
      <slot>
        <!-- Default Content:Display when the parent component does not pass data -->
        <p>No content</p>
      </slot>
    </div>
    <div v-if="$slots.footer" class="card-footer">
      <slot name="footer" />
    </div>
  </div>
</template>

<style scoped>
.card {
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  overflow: hidden;
}
.card-header, .card-footer {
  background: #f9fafb;
  padding: 1rem;
}
.card-body {
  padding: 1rem;
}
</style>
▶ Experimente

▶ Exemplo: 2. 5 maneiras de usar o BaseCard

VUE
<!-- 1. The simplest(Transmit Only body) -->
<BaseCard>
  <p>Simple content</p>
</BaseCard>

<!-- 2. With header -->
<BaseCard>
  <template #header>
    <h3>Product Name</h3>
  </template>
  <p>Product description</p>
</BaseCard>

<!-- 3. Complete header + body + footer -->
<BaseCard>
  <template #header>
    <h3>Order #12345</h3>
  </template>
  <p>Total: $99.99</p>
  <template #footer>
    <button>View Details</button>
  </template>
</BaseCard>

<!-- 4. Complete Syntax(v-slot) -->
<BaseCard>
  <template v-slot:header>
    <h3>Header</h3>
  </template>
  <template v-slot:default>
    <p>Body</p>
  </template>
  <template v-slot:footer>
    <p>Footer</p>
  </template>
</BaseCard>

<!-- 5. Scope Slot(Receive Data) -->
<UserCard :user="user">
  <template #actions="{ user }">
    <button @click="edit(user)">Edit</button>
    <button @click="remove(user)">Delete</button>
  </template>
</UserCard>
▶ Experimente

▶ Exemplo: 3. Exemplo completo de slots de escopo

VUE
<!-- Child component:DataTable.vue -->
<template>
  <table>
    <thead>
      <tr>
        <th v-for="col in columns" :key="col.key">{{ col.label }}</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(row, index) in data" :key="row.id">
        <td v-for="col in columns" :key="col.key">
          <!-- Scope Slot:Allow the parent component to customize cell rendering -->
          <slot :name="`cell-${col.key}`" :row="row" :index="index" :value="row[col.key]">
            <!-- Default:Show Original Values -->
            {{ row[col.key] }}
          </slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>

<script setup>
defineProps({
  columns: { type: Array, required: true },
  data: { type: Array, required: true }
})
</script>
▶ Experimente
VUE
<!-- Using Parent Components DataTable -->
<DataTable :columns="columns" :data="users">
  <!-- Custom status column -->
  <template #cell-status="{ row }">
    <span :class="['badge', row.status]">{{ row.status }}</span>
  </template>
  
  <!-- Custom actions column -->
  <template #cell-actions="{ row }">
    <button @click="edit(row)">Edit</button>
  </template>
</DataTable>

▶ Exemplo: 4. Referência rápida para 5 erros comuns

Erro Sintoma Solução
Nome do slot com erro ortográfico Conteúdo não exibido Verifique se <slot name="..."> e #name correspondem
Vários elementos raiz no componente pai Aviso O componente pai está envolto em <template #name>
v-slot (sintaxe antiga) Compatível com o Vue 2 Use v-slot:header ou #header em vez disso
Slot padrão não passado Componente pai não exibido Verifique o conteúdo <Component /> no componente pai
Os slots de escopo não são desestruturados Os modelos são prolixos Desestruturação com { user, index }

▶ Exemplo: 5. 5 Comparações de desempenho

Sintaxe Compilação Desempenho Recomendações
<slot /> Estático ⭐⭐⭐⭐⭐ Padrão
<slot name="x" /> Estático ⭐⭐⭐⭐⭐ Nomeado
<slot :x="x" /> Estático ⭐⭐⭐⭐ Alcance
v-if="$slots.x" Condição ⭐⭐⭐⭐ Detectado
useSlots() Tempo de execução ⭐⭐⭐ Usado em JS

▶ Exemplo: 6. 5 Principais usos de $slots

VUE
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
</script>

<template>
  <div class="wrapper">
    <!-- 1. Check if the slot exists -->
    <div v-if="slots.header">Has header</div>
    
    <!-- 2. List all slot names -->
    <div v-for="name in Object.keys(slots)" :key="name">
      <slot :name="name" />
    </div>
    
    <!-- 3. Passthrough attribute -->
    <input v-bind="attrs" />
    
    <!-- 4. Conditional Rendering -->
    <template v-if="slots.default">
      <div class="content">
        <slot />
      </div>
    </template>
    
    <!-- 5. Default Slot Packaging -->
    <div class="default-wrapper">
      <slot>
        <p>Default fallback</p>
      </slot>
    </div>
  </div>
</template>
▶ Experimente

❓ Perguntas Frequentes

P: Qual é a diferença entre slots e props? R: Os props passam dados (qualquer valor em JavaScript), enquanto os slots passam conteúdo (fragmentos de HTML/componentes). Os props são adequados para opções de configuração, enquanto os slots são adequados para personalização de conteúdo.

P: Quando os slots de escopo devem ser usados? R: Quando um componente pai precisa personalizar sua renderização com base nos dados de um componente filho. Por exemplo: renderizar colunas de tabela, renderizar itens de lista ou organizar o layout do conteúdo de cartões.

P: Qual é a diferença entre v-slot e a forma abreviada #? R: São totalmente equivalentes; #header is a shorthand for v-slot:header. Recomendamos usar a forma abreviada (é mais concisa).

P: É possível nomear o slot padrão? R: Não é necessário nomear o slot padrão; <slot /> é o valor padrão. Se for absolutamente necessário nomeá-lo, use <slot name="default" />; o componente pai pode usar <template #default> ou simplesmente passar o conteúdo diretamente.

P: É possível passar slots como props? R: Não é possível passar um slot diretamente como prop. No entanto, é possível passar um VNode ou uma função de renderização como prop (uso avançado). Na maioria dos casos, basta usar <slot>.

P: Como faço para detectar o slot de um componente filho em um componente pai? R: Use $slots (modelo) ou useSlots() (JS). O componente filho expõe o slot usando <slot>, e o componente pai o preenche usando <template #name>.

P: Os slots nomeados podem ter conteúdo padrão? R: Sim. <slot name="header"><h3>Default Header</h3></slot>. Esse conteúdo é exibido quando o componente pai não passa um cabeçalho.


📖 Resumo


📝 Exercícios

  1. Questões básicas (Dificuldade: ⭐)

    Implemente um componente Button simples:

    • BaseButton.vue: Slot padrão + 3 variantes (principal/sucesso/perigo)
    • props: variant (String)
    • Componente pai: Testar 3 variantes + conteúdo personalizado do slot
  2. Problemas avançados (Dificuldade: ⭐⭐)

    Implemente o componente BaseLayout:

    • Componentes filhos: 3 slots nomeados (cabeçalho / padrão / rodapé)
    • Componente pai: use o BaseLayout para implementar a página do painel
    • Use $slots para verificar se o slot existe
    • Adicionar conteúdo padrão (alternativo)
  3. Problema de desafio (Dificuldade: ⭐⭐⭐)

    Implementar um sistema completo de componentes DataTable:

    1. DataTable.vue: Recebe colunas + dados e renderiza a tabela
    2. Espaço de escopo: #cell-{key} Permite que os componentes pai personalizem as células
    3. Slot padrão: exibe o valor original quando o componente pai não fornece um
    4. Exemplo de componente pai: Tabela de usuários + Tabela de pedidos — Dois métodos diferentes de exibição de colunas
    5. Desempenho: o v-memo armazena as linhas em cache (evita a redesenho quando os dados permanecem inalterados)
    6. Tipagem forte do TypeScript
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%