Suggest an editImprove this articleRefine the answer for “What are slots in Vue?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Slots** let a parent component inject custom content into named placeholders in a child component's template. Named slots target specific areas, scoped slots let the child share its own data back via `v-slot="{ item }"`. ```vue <Card> <template #header><h1>Title</h1></template> <p>Body content</p> <template #footer><button>Action</button></template> </Card> ``` **Key:** use slots when the parent controls structure; use props when passing data only.Shown above the full answer for quick recall.Answer (EN)Image**Slots** let a parent component inject custom content into specific placeholders defined in a child component's template. ## Theory ### TL;DR - Think of slots like USB ports: the child defines the ports, the parent plugs in whatever content it wants - Three types: default (unnamed), named (targeted by name), scoped (child passes data back to parent's content) - Slots pass **content** (HTML, components); props pass **data** (strings, objects) - Decision rule: parent needs to control the UI? Use a slot. Passing data only? Use props. - Vue 3 syntax: `#name` or `v-slot:name` (the old `slot="name"` attribute is deprecated) ### Quick example ```vue <!-- Alert.vue --> <template> <div class="alert"> <slot name="icon">⚠️</slot> <!-- named slot with fallback --> <slot>Default message</slot> <!-- default slot --> </div> </template> ``` ```vue <!-- Parent --> <Alert> <template #icon>🚨</template> <p>Custom urgent message!</p> </Alert> <!-- Renders: 🚨 Custom urgent message! --> ``` If the parent leaves out `#icon`, the child's fallback `⚠️` renders instead. That fallback content inside `<slot>` is what shows when nothing is provided. ### Slots vs props Props carry data: strings, numbers, objects. Slots carry **structure**: HTML, other components, anything you write in a template. You cannot pass a `<CustomButton>` or a styled paragraph through a prop without it turning into an escaped string. Slots solve exactly that problem. Slot content also lives in the parent's scope. The parent writes it, so it has access to parent variables and methods. Scoped slots add a reverse channel where the child shares its own data back up. ### Default, named, and scoped **Default slot** is the unnamed catch-all. Any content placed directly inside a component tag fills it. **Named slots** let you target multiple placeholders independently. A layout component can have `header`, `default`, and `footer` slots, each controlled separately. ```vue <!-- Layout.vue --> <template> <header><slot name="header" /></header> <main><slot /></main> <footer><slot name="footer" /></footer> </template> ``` ```vue <!-- Usage --> <Layout> <template #header><h1>Page Title</h1></template> <p>Main content here</p> <template #footer><p>© 2025</p></template> </Layout> ``` **Scoped slots** are the most flexible. The child exposes data through the slot, and the parent decides how to render it. Classic use case: a reusable list that owns the data but delegates rendering to the parent. ```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: 'Alice' }, { id: 2, name: 'Bob' }]) </script> ``` ```vue <!-- Dashboard.vue --> <UserList> <template #default="{ item }"> <strong>{{ item.name }}</strong> <button @click="editUser(item)">Edit</button> </template> </UserList> ``` The list handles iteration and data. The parent controls presentation. In most dashboard projects I have worked on, this exact pattern shows up in every data table. It is also why component libraries like Vuetify and Element Plus are so configurable without requiring you to fork them. ### When to use - Generic container (card, modal, panel) → default slot - Multiple distinct areas (header, body, footer) → named slots - Child owns data, parent controls rendering (tables, lists, dropdowns) → scoped slots - Passing only strings or primitives → [props](/questions/what-are-props-in-vue) are enough - Deep data sharing without prop drilling → [provide/inject](/questions/provide-inject-in-vue) ### How Vue handles slots internally Vue's template compiler scans `<slot>` tags and creates slot anchors in the child's render function. At runtime, parent content compiles into a separate render function that gets injected at those anchors. Scoped slots work the same way, except the child passes a props object when calling the slot function. In Vue 3, reactive slot data flows through Proxy, so changes in child data automatically update rendered slot content without re-rendering the whole tree. You can check whether a slot was provided using `$slots.name` in templates, or `useSlots()` in `<script setup>`: ```vue <script setup> import { useSlots } from 'vue' const slots = useSlots() </script> <template> <slot name="tab2" /> <p v-if="!slots.tab2">No content provided</p> </template> ``` ### Common mistakes **Passing HTML through a prop:** ```vue <!-- Wrong --> <Child :content="'<p>Hello</p>'" /> <!-- Renders as an escaped string, not HTML --> ``` ```vue <!-- Correct --> <Child><p>Hello</p></Child> ``` Props serialize values. HTML passed as a string renders as `<p>Hello</p>`. **Using deprecated Vue 2 slot syntax in Vue 3:** ```vue <!-- Wrong in Vue 3 --> <Child slot="header">Content</Child> ``` ```vue <!-- Correct --> <Child v-slot:header>Content</Child> <!-- shorthand --> <Child #header>Content</Child> ``` The `slot` attribute is deprecated. Vue 3's compiler expects `v-slot:name` or `#name`. **Trying to mutate scoped slot props:** ```vue <!-- Wrong: mutation does not persist --> <List v-slot="{ item }"> {{ item.name = 'Changed' }} </List> ``` Slot props are read-only in the parent template. They arrive through a Proxy that ignores mutations. To update child data, emit an event and handle it in the parent: ```vue <List @update="handleUpdate" v-slot="{ item }"> <button @click="$emit('update', item)">Update</button> </List> ``` **Unnamed nested slots colliding:** When you nest components without naming inner slots, multiple `<slot />` targets fight over the same default. Always name slots when a component has more than one content area. ### Real-world usage - Vuetify: `<v-card><template #text>Custom content</template></v-card>` - Element Plus: `<el-table><template #default="{ row }">...</template></el-table>` - Nuxt UI: layout components expose `<slot name="hero" />` for page sections - PrimeVue: `<DataTable><template #body="slotProps">...</template></DataTable>` Scoped slots are the foundation of data-display components across every major Vue UI library. ### Follow-up questions **Q:** What is the difference between a default slot and a named slot? **A:** Default fills the unnamed `<slot />`. Named slots target a specific `<slot name="x" />` via `#x` or `v-slot:x`. One component can have both at the same time. **Q:** Can slot content access the child component's data? **A:** Not directly. Slot content lives in the parent's scope and can only see parent variables. Scoped slots bridge that gap: `<slot :item="user" />` exposes `user` to the parent via `v-slot="{ item }"`. **Q:** How do you detect whether a slot was filled by the parent? **A:** In templates, check `$slots.name`. In `<script setup>`, call `useSlots()` from the Composition API and inspect the returned object reactively. **Q:** What changed between Vue 2 and Vue 3 slot syntax? **A:** Vue 2 used `slot="name"` on elements and `slot-scope="data"` for scoped slots. Vue 3 replaced both with `v-slot:name="data"` (shorthand `#name="data"`). The old syntax still exists but is deprecated and may be removed. **Q:** How does Vue optimize scoped slots when used with Suspense or async components? **A:** In Vue 3, slot content can suspend independently from the parent. A child can wrap a slot in `<Suspense>` to show a fallback while async content inside the slot resolves. Reactive slot data propagates through Proxy, so only the affected slot re-renders when child data changes, not the entire component tree. ## Examples ### Basic: Card component with named slots and fallbacks ```vue <!-- Card.vue --> <template> <div class="card"> <div class="card-header"> <slot name="header">Default Title</slot> </div> <div class="card-body"> <slot /> </div> <!-- footer div only exists in DOM when parent provides content --> <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>Best laptop for developers.</p> <p>Price: $2,499</p> <template #footer> <button @click="addToCart">Add to cart</button> </template> </Card> ``` The `v-if="$slots.footer"` check removes the entire `div` from the DOM when the parent does not provide footer content. Not an empty block, a completely absent element. This matters for CSS layouts that target direct children. ### Intermediate: Reusable data table with scoped slots A common dashboard pattern: the table owns structure and iteration, each column delegates rendering to the parent. ```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 exposes both the full row and the cell value --> <slot :name="col.key" :row="row" :value="row[col.key]"> {{ row[col.key] }} <!-- fallback: plain text --> </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)">Edit</button> <button @click="deleteUser(row.id)">Delete</button> </template> </DataTable> ``` Columns without a matching slot fall back to plain text automatically. This is the same architecture `<el-table>` in Element Plus uses. ### Senior: Reactive slot detection with useSlots ```vue <!-- DashboardWidget.vue --> <template> <div class="widget"> <div class="widget-header"> <slot name="title"> <span>{{ defaultTitle }}</span> </slot> <!-- actions bar only mounts when the parent fills it --> <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: 'Widget' } }) const slots = useSlots() </script> ``` ```vue <!-- AdminPanel.vue --> <DashboardWidget default-title="Traffic"> <template #actions> <button @click="refresh">Refresh</button> <button @click="exportData">Export</button> </template> <TrafficChart :data="chartData" /> </DashboardWidget> ``` `useSlots()` returns a reactive object. If the parent adds or removes `#actions` dynamically, the widget reacts without any extra logic. The approach keeps empty wrapper divs out of the rendered HTML, which matters when CSS grid or flex rules target direct children.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.