Skip to main content

Що таке слоти у Vue?

Slots (слоти) дозволяють батьківському компоненту вставляти довільний контент у заздалегідь визначені місця в шаблоні дочірнього компонента.

Теорія

TL;DR

  • Слоти - це як USB-порти: дочірній компонент визначає порти, батьківський підключає будь-який контент
  • Три типи: default (безіменний), named (з іменем), scoped (дочірній передає дані батьківському)
  • Слоти передають контент (HTML, компоненти); props передають дані (рядки, об'єкти)
  • Правило вибору: батько контролює структуру UI? Слот. Тільки дані? Props.
  • Синтаксис Vue 3: #name або v-slot:name (стара форма slot="name" є застарілою)

Швидкий приклад

vue
<!-- Alert.vue --> <template> <div class="alert"> <slot name="icon">⚠️</slot> <!-- named slot з fallback --> <slot>Повідомлення за замовчуванням</slot> <!-- default slot --> </div> </template>
vue
<!-- Батько --> <Alert> <template #icon>🚨</template> <p>Термінове повідомлення!</p> </Alert> <!-- Результат: 🚨 Термінове повідомлення! -->

Якщо батько не надає #icon, показується fallback ⚠️ з дочірнього компонента. Контент всередині <slot> - це і є той fallback.

Слоти проти props

Props переносять дані: рядки, числа, об'єкти. Слоти переносять структуру: HTML, інші компоненти, все що можна написати в шаблоні. Передати <CustomButton> або стилізований абзац через prop неможливо без того, щоб це перетворилось на екранований рядок. Слоти вирішують саме цю проблему.

Контент слота живе у скоупі батька. Батько його пише, тому має доступ до своїх змінних і методів. Scoped slots додають зворотний зв'язок: дочірній компонент ділиться власними даними з батьківським.

Default, named і scoped слоти

Default slot - безіменний слот. Будь-який контент, розміщений безпосередньо між тегами компонента, потрапляє в нього.

Named slots дозволяють заповнювати кілька місць окремо. Компонент макету може мати слоти header, default і footer, кожен із яких батько контролює незалежно.

vue
<!-- Layout.vue --> <template> <header><slot name="header" /></header> <main><slot /></main> <footer><slot name="footer" /></footer> </template>
vue
<!-- Використання --> <Layout> <template #header><h1>Заголовок сторінки</h1></template> <p>Основний контент</p> <template #footer><p>© 2025</p></template> </Layout>

Scoped slots - найгнучкіший тип. Дочірній компонент передає дані через слот, а батьківський вирішує, як їх відобразити. Класичний приклад: список, який сам зберігає дані, але делегує рендеринг батьку.

vue
<!-- UserList.vue --> <template> <ul> <li v-for="user in users" :key="user.id"> <slot :item="user" /> </li> </ul> </template> <script setup> import { ref } from 'vue' const users = ref([{ id: 1, name: 'Аліса' }, { id: 2, name: 'Боб' }]) </script>
vue
<!-- Dashboard.vue --> <UserList> <template #default="{ item }"> <strong>{{ item.name }}</strong> <button @click="editUser(item)">Редагувати</button> </template> </UserList>

Список відповідає за ітерацію і дані. Батько контролює відображення. У більшості дашбордів, з якими доводилось працювати, цей патерн зустрічається в кожній таблиці даних - саме завдяки йому бібліотеки Vuetify та Element Plus такі гнучкі без необхідності їх форкати.

Коли використовувати

  • Загальний контейнер (картка, модалка, панель) → default slot
  • Кілька різних областей (header, body, footer) → named slots
  • Дочірній компонент зберігає дані, батько контролює рендеринг (таблиці, списки, дропдауни) → scoped slots
  • Передаються тільки рядки або примітиви → достатньо props
  • Глибоке передавання даних без prop drilling → provide/inject

Як Vue обробляє слоти всередині

Компілятор шаблонів Vue сканує теги <slot> і створює точки вставки в render-функції дочірнього компонента. Під час рендерингу контент батьківського компонента компілюється в окрему render-функцію, яка підставляється в ці точки. Scoped slots працюють так само, але дочірній компонент передає об'єкт з даними при виклику функції слота. У Vue 3 реактивні дані слота передаються через Proxy, тому зміни в дочірньому компоненті автоматично оновлюють контент слота без ре-рендеру всього дерева.

Перевірити наявність слота можна через $slots.name у шаблоні або useSlots() у <script setup>:

vue
<script setup> import { useSlots } from 'vue' const slots = useSlots() </script> <template> <slot name="tab2" /> <p v-if="!slots.tab2">Контент не надано</p> </template>

Типові помилки

Передача HTML через prop:

vue
<!-- Неправильно --> <Child :content="'<p>Привіт</p>'" /> <!-- Рендериться як екранований рядок, не HTML -->
vue
<!-- Правильно --> <Child><p>Привіт</p></Child>

Props серіалізують значення. HTML переданий як рядок відрендериться як &lt;p&gt;Привіт&lt;/p&gt;.

Використання застарілого синтаксису Vue 2 у Vue 3:

vue
<!-- Неправильно для Vue 3 --> <Child slot="header">Контент</Child>
vue
<!-- Правильно --> <Child v-slot:header>Контент</Child> <!-- або скорочено --> <Child #header>Контент</Child>

Атрибут slot є застарілим. Компілятор Vue 3 очікує v-slot:name або #name.

Спроба змінити дані scoped slot:

vue
<!-- Неправильно: мутація не збережеться --> <List v-slot="{ item }"> {{ item.name = 'Змінено' }} </List>

Slot props доступні тільки для читання в шаблоні батька. Вони передаються через Proxy, який ігнорує мутації. Щоб оновити дані, потрібно emit-нути подію і обробити її в батьку:

vue
<List @update="handleUpdate" v-slot="{ item }"> <button @click="$emit('update', item)">Оновити</button> </List>

Безіменні вкладені слоти:

Коли компоненти вкладені і жоден слот не названий, кілька <slot /> конкурують за один default. Завжди називай слоти, якщо компонент має більше однієї контентної зони.

Де використовується

  • Vuetify: <v-card><template #text>Custom content</template></v-card>
  • Element Plus: <el-table><template #default="{ row }">...</template></el-table>
  • Nuxt UI: компоненти макетів використовують <slot name="hero" /> для секцій сторінки
  • PrimeVue: <DataTable><template #body="slotProps">...</template></DataTable>

Scoped slots - це основа компонентів відображення даних у кожній великій UI-бібліотеці для Vue.

Follow-up питання

Q: Яка різниця між default і named слотом?
A: Default заповнює безіменний <slot />. Named слоти відповідають конкретному <slot name="x" /> через #x або v-slot:x. В одному компоненті можуть бути обидва типи одночасно.

Q: Чи може контент слота звертатись до даних дочірнього компонента?
A: Не напряму. Контент слота живе у скоупі батька і бачить тільки батьківські змінні. Scoped slots вирішують це: <slot :item="user" /> робить user доступним у батьку через v-slot="{ item }".

Q: Як перевірити, чи батько заповнив слот?
A: У шаблоні - через $slots.name. У <script setup> - через useSlots() з Composition API, який повертає реактивний об'єкт із наявними слотами.

Q: Що змінилось у синтаксисі слотів між Vue 2 і Vue 3?
A: Vue 2 використовував slot="name" на елементах і slot-scope="data" для scoped слотів. Vue 3 замінив обидва на v-slot:name="data" (скорочено #name="data"). Старий синтаксис ще працює, але є застарілим і може бути видалений.

Q: Як Vue оптимізує scoped slots при використанні з Suspense або async-компонентами?
A: У Vue 3 контент слота може призупинятись незалежно від батька. Дочірній компонент може огорнути слот у <Suspense> для показу fallback поки async-контент завантажується. Реактивні дані слота проходять через Proxy, тому при зміні даних в дочірньому компоненті оновлюється тільки відповідний слот, а не все дерево компонентів.

Приклади

Базовий: Компонент картки з named slots і fallback

vue
<!-- Card.vue --> <template> <div class="card"> <div class="card-header"> <slot name="header">Заголовок за замовчуванням</slot> </div> <div class="card-body"> <slot /> </div> <!-- footer div існує в DOM тільки якщо батько надав контент --> <div class="card-footer" v-if="$slots.footer"> <slot name="footer" /> </div> </div> </template>
vue
<!-- ProductPage.vue --> <Card> <template #header> <h2>MacBook Pro</h2> </template> <p>Найкращий ноутбук для розробників.</p> <p>Ціна: $2,499</p> <template #footer> <button @click="addToCart">Додати в кошик</button> </template> </Card>

v-if="$slots.footer" прибирає весь div з DOM, якщо батько не надав контент для footer. Не порожній блок, а повна відсутність елемента. Це важливо, коли CSS grid або flex правила адресують прямих дітей.

Середній: Таблиця даних зі scoped slots

Поширений патерн для дашбордів: таблиця відповідає за структуру та ітерацію, рендеринг кожного стовпця делегується батьку.

vue
<!-- 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 in rows" :key="row.id"> <td v-for="col in columns" :key="col.key"> <!-- scoped slot передає і весь рядок, і значення клітинки --> <slot :name="col.key" :row="row" :value="row[col.key]"> {{ row[col.key] }} <!-- за замовчуванням: просто текст --> </slot> </td> </tr> </tbody> </table> </template> <script setup> defineProps({ columns: Array, rows: Array }) </script>
vue
<!-- UsersDashboard.vue --> <DataTable :columns="cols" :rows="users"> <!-- кастомний рендер для стовпця статусу --> <template #status="{ value }"> <span :class="value === 'active' ? 'badge-green' : 'badge-red'"> {{ value }} </span> </template> <!-- кастомний рендер для стовпця дій --> <template #actions="{ row }"> <button @click="editUser(row)">Редагувати</button> <button @click="deleteUser(row.id)">Видалити</button> </template> </DataTable>

Стовпці без кастомного слота автоматично показують звичайний текст. Саме так влаштований <el-table> в Element Plus.

Senior: Реактивне визначення слотів через useSlots

vue
<!-- DashboardWidget.vue --> <template> <div class="widget"> <div class="widget-header"> <slot name="title"> <span>{{ defaultTitle }}</span> </slot> <!-- панель дій монтується тільки якщо батько її заповнив --> <div v-if="slots.actions" class="widget-actions"> <slot name="actions" /> </div> </div> <div class="widget-body"> <slot /> </div> </div> </template> <script setup> import { useSlots } from 'vue' defineProps({ defaultTitle: { type: String, default: 'Віджет' } }) const slots = useSlots() </script>
vue
<!-- AdminPanel.vue --> <DashboardWidget default-title="Трафік"> <template #actions> <button @click="refresh">Оновити</button> <button @click="exportData">Експорт</button> </template> <TrafficChart :data="chartData" /> </DashboardWidget>

useSlots() повертає реактивний об'єкт. Якщо батько динамічно додає або прибирає #actions, віджет реагує без додаткової логіки. Цей підхід прибирає порожні wrapper div з HTML, що важливо для CSS grid макетів, які адресують прямих дітей.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?