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
- Uso básico do slot padrão
<slot /> - Abreviações para os slots denominados
<slot name="header" />ev-slot:header - Slots de escopo: os componentes filhos passam dados para os slots do componente pai
<template v-slot>Sintaxe (Vue 2.6+)- Sintaxe de nomes e abreviações dinâmicos de slots
- Conteúdo padrão do slot (conteúdo alternativo)
$slots/useSlotshook
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:
<!-- ❌ 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
<!-- 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>
<!-- 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:
- Número de componentes: 5 → 1 (-80%)
- Novo tipo de cartão: 1 linha de código de template
- Custos de manutenção: basta alterar um estilo para que mais de 5 cartões sejam atualizados simultaneamente
- Reutilização: O BaseCard pode ser utilizado em todos os cenários relacionados a cartões
3. Slot padrão
(1) Uso básico
<!-- BaseCard.vue Child component -->
<template>
<div class="card">
<!-- Slot: The parent component can contain any content -->
<slot />
</div>
</template>
<!-- App.vue Parent Component -->
<template>
<BaseCard>
<h3>Hello</h3>
<p>This is the card content</p>
</BaseCard>
</template>
Resultados da renderização:
<div class="card">
<h3>Hello</h3>
<p>This is the card content</p>
</div>
(2) Conteúdo padrão (alternativo)
<!-- 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>
<!-- 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
<!-- 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)
<!-- 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
<!-- 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
<!-- 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>
<!-- 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
<!-- 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>
<!-- 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:
<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
<!-- 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>
<!-- 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
<!-- 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)
<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)
<!-- 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>
▶ Exemplo: 2. 5 maneiras de usar o BaseCard
<!-- 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>
▶ Exemplo: 3. Exemplo completo de slots de escopo
<!-- 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>
<!-- 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
<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>
❓ 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-slote a forma abreviada#? R: São totalmente equivalentes;#headeris a shorthand forv-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) ouuseSlots()(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
- Os slots são a API de distribuição de conteúdo do Vue: os componentes pai controlam o conteúdo, enquanto os componentes filhos controlam o posicionamento
- 3 tipos de slots: slot padrão (
<slot />), slot nomeado (name="x") e slot com escopo (:x="x") - O componente pai passa um slot nomeado usando
<template #name>ou a abreviação#name - Os slots de escopo permitem que os componentes filhos transmitam dados aos componentes pais (propriedades de slot)
<slot>Conteúdo padrão (alternativo) exibido quando o componente pai não passa nenhum conteúdo$slots/useSlots()Verificar se o slot existe- Trabalhando com slots e props: os props passam a configuração, enquanto os slots passam o conteúdo
📝 Exercícios
-
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
-
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
$slotspara verificar se o slot existe - Adicionar conteúdo padrão (alternativo)
-
Problema de desafio (Dificuldade: ⭐⭐⭐)
Implementar um sistema completo de componentes DataTable:
- DataTable.vue: Recebe colunas + dados e renderiza a tabela
- Espaço de escopo:
#cell-{key}Permite que os componentes pai personalizem as células - Slot padrão: exibe o valor original quando o componente pai não fornece um
- Exemplo de componente pai: Tabela de usuários + Tabela de pedidos — Dois métodos diferentes de exibição de colunas
- Desempenho: o v-memo armazena as linhas em cache (evita a redesenho quando os dados permanecem inalterados)
- Tipagem forte do TypeScript