---
title: "Slots explained: default, named and scoped · vuejs.school"
description: "Props pass data into a component, slots pass markup. Default, named and scoped slots with a Card and a List, and where v-slot is allowed to go."
canonical_url: "https://vuejs.school/community/blog/slots-explained-default-named-and-scoped"
meta:
  "og:description": "Props pass data into a component, slots pass markup. Default, named and scoped slots with a Card and a List, and where v-slot is allowed to go."
  "og:image": "https://vuejs.school/_og/d/a_~U2xvdHMgZXhwbGFpbmVkOiBkZWZhdWx0LCBuYW1lZCBhbmQgc2NvcGVkIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~U2xvdHMgZXhwbGFpbmVkOiBkZWZhdWx0LCBuYW1lZCBhbmQgc2NvcGVk%2Cdescription_~UHJvcHMgcGFzcyBkYXRhIGludG8gYSBjb21wb25lbnQsIHNsb3RzIHBhc3MgbWFya3VwLiBEZWZhdWx0LCBuYW1lZCBhbmQgc2NvcGVkIHNsb3RzIHdpdGggYSBDYXJkIGFuZCBhIExpc3QsIGFuZCB3aGVyZSB2LXNsb3QgaXMgYWxsb3dlZCB0byBnby4%2Ceyebrow_~RGVlcCBkaXZlIMK3IDUgbWluIHJlYWQ%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9zbG90cy1leHBsYWluZWQtZGVmYXVsdC1uYW1lZC1hbmQtc2NvcGVkIg%2Cs_oc8bqQJC2vVV3thQ.png"
  "og:image:alt": "Slots explained: default, named and scoped · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Slots explained: default, named and scoped · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~U2xvdHMgZXhwbGFpbmVkOiBkZWZhdWx0LCBuYW1lZCBhbmQgc2NvcGVkIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~U2xvdHMgZXhwbGFpbmVkOiBkZWZhdWx0LCBuYW1lZCBhbmQgc2NvcGVk%2Cdescription_~UHJvcHMgcGFzcyBkYXRhIGludG8gYSBjb21wb25lbnQsIHNsb3RzIHBhc3MgbWFya3VwLiBEZWZhdWx0LCBuYW1lZCBhbmQgc2NvcGVkIHNsb3RzIHdpdGggYSBDYXJkIGFuZCBhIExpc3QsIGFuZCB3aGVyZSB2LXNsb3QgaXMgYWxsb3dlZCB0byBnby4%2Ceyebrow_~RGVlcCBkaXZlIMK3IDUgbWluIHJlYWQ%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9zbG90cy1leHBsYWluZWQtZGVmYXVsdC1uYW1lZC1hbmQtc2NvcGVkIg%2Cs_oc8bqQJC2vVV3thQ.png"
  "twitter:image:alt": "Slots explained: default, named and scoped · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## Pass markup in with the default slot

Inside the child, `<slot />` marks the spot where the parent's content lands. Anything the parent writes between the component's opening and closing tags fills it.

*components/Card.vue*

```
<template>
  <section class="card">
    <slot />
  </section>
</template>

<style scoped>
.card { border: 1px solid #dfe3e9; border-radius: 12px; padding: 16px; }
</style>
```

*App.vue*

```
<script setup lang="ts">
import Card from './components/Card.vue'
</script>

<template>
  <Card>
    <h2>Welcome back</h2>
    <p>You have three reviews due today.</p>
  </Card>
</template>
```

The `h2` and the `p` render inside the `section`, but they are compiled as part of the parent's template. So they can use the parent's data, and the parent's scoped styles apply to them, not the child's. The child only decides where they go.

Text placed inside the `<slot>` tag itself is fallback content, shown only when the parent gives nothing: `<slot>Nothing here yet.</slot>`. Use it for empty states, so a `Card` with no content still says something.

## Fill several spots with named slots

A card usually has more than one spot: a header, a body, a footer. Give each `<slot>` a `name`. The slot without a name is still the default. Here is the new template for `Card.vue` (the `<style>` block stays as it was):

*components/Card.vue*

```
<template>
  <section class="card">
    <header><slot name="header">Untitled</slot></header>
    <slot />
    <footer v-if="$slots.footer"><slot name="footer" /></footer>
  </section>
</template>
```

The parent fills a named slot with a `<template>` tag carrying `#` and the slot's name. `#header` is short for `v-slot:header`, the same way `@click` is short for `v-on:click`. Content that is not inside any `<template #...>` goes to the default slot.

*App.vue*

```
<script setup lang="ts">
import Card from './components/Card.vue'
</script>

<template>
  <Card>
    <template #header>Welcome back</template>
    <p>You have three reviews due today.</p>
    <template #footer>Last visit: Tuesday</template>
  </Card>
</template>
```

The two named slots handle an empty slot in two ways. `header` has fallback text, so a card without a header shows "Untitled". The `footer` slot sits inside a `<footer>` element with `v-if="$slots.footer"`. `$slots` is an object with one entry per slot the parent actually filled, so when nobody passes a footer, the `footer` element is not rendered at all, along with any padding or border you give it. Without the `v-if`, an empty footer would still take up that space.

## Hand data back with scoped slots

So far, markup has flowed one way, from the parent into the child. Sometimes data has to flow the other way. A `List` component owns the array, the `v-for` and the keys, and it should. But the parent knows how one item should look. The child needs to hand each item back to the parent so the parent can render it. That is a scoped slot: a slot that comes with data attached.

*components/List.vue*

```
<script setup lang="ts">
defineProps<{ items: { id: number; name: string }[] }>()
</script>

<template>
  <ul>
    <li v-for="(item, index) in items" :key="item.id">
      <slot :item="item" :index="index">{{ item.name }}</slot>
    </li>
  </ul>
</template>
```

Binding attributes on `<slot>`, here `:item` and `:index`, turns them into slot props. The parent receives them in the `#default` binding and can destructure them, exactly like a function parameter:

*App.vue*

```
<script setup lang="ts">
import List from './components/List.vue'

const fruits = [{ id: 1, name: 'Mango' }, { id: 2, name: 'Pear' }]
</script>

<template>
  <List :items="fruits">
    <template #default="{ item, index }">
      <strong>{{ index + 1 }}.</strong> {{ item.name }}
    </template>
  </List>
</template>
```

Read `#default="{ item, index }"` as "for the default slot, give me `item` and `index`".

The fallback `{{ item.name }}` inside the child's `<slot>` still applies: a parent that writes `<List :items="fruits" />` with nothing inside gets plain names.

When the parent fills only the default slot, the `<template>` wrapper is optional. `<List :items="fruits" v-slot="{ item }">` puts the binding on the component tag itself and does the same job in one line.

UI libraries use this pattern to let you customise what they render, from a table's cells to a select's options. Once you have written one scoped slot yourself, their documentation stops looking like magic.

## Put v-slot on a template, not an element

`v-slot` and its `#` shorthand belong on a component tag or on a `<template>` tag. Nowhere else. The natural thing to try, after seeing `v-for` on an `li`, is to put the slot binding on the element you want to repeat:

*App.vue**Breaks***

```
<template>
  <List :items="fruits">
    <li #default="{ item }">{{ item.name }}</li>
  </List>
</template>
```

The compiler refuses with `v-slot can only be used on components or <template> tags.` The fix is to move the binding to a `<template>` tag and drop the `li`, because the child already renders one around each item:

*App.vue**Works***

```
<template>
  <List :items="fruits">
    <template #default="{ item }">{{ item.name }}</template>
  </List>
</template>
```

The error reads oddly the first time, so remember the rule behind it: a slot binding says which slot the content goes into, and only a `<template>` or the component itself can carry that. An ordinary element is content, not a container for content.

## Choose between a slot and a prop

Ask what the parent is passing:

- **A value** the child will display or compute with, such as a string, a number or an array: use a prop. The child stays in charge of the markup, and the prop is easy to type and validate.
- **Markup**, with its own tags, components or event handlers: use a slot.
- **Markup that needs the child's data**, such as how to show each item of a list: use a scoped slot.

## Type slot props with defineSlots

Vue's language tools, which power the official Vue extension for VS Code and the `vue-tsc` type checker, already read slot props from the `<slot>` tags in the child's template. With no extra code, the parent's `#default="{ item }"` is type-checked and your editor autocompletes `item.name`. Since Vue 3.3, `defineSlots()` lets you declare those types in the script yourself. It is a compiler macro, like `defineProps`, so nothing is imported. Add it to `List.vue` (the template stays the same):

*components/List.vue*

```
<script setup lang="ts">
interface Item { id: number; name: string }

defineProps<{ items: Item[] }>()

defineSlots<{
  default(props: { item: Item; index: number }): any
}>()
</script>
```

Each key is a slot name, and each value is a function type whose single parameter lists the slot props. Vue currently ignores the return type, so `any` is the usual choice. A slot that takes no props, such as the `Card`'s `header`, is declared as `header(): any`.

For a small component, the inferred types are enough. For a `List` or a `Table` that other people will use, the declaration is worth writing. Anyone opening `List.vue` sees the slots and their props at the top of the file. And if a `<slot>` tag stops passing a declared prop, `vue-tsc` reports the error in the component itself, not in the parents that use it.

**Check it stuck**3 questions from the Slots quiz.

[**Practice now**](https://vuejs.school/learn/beginner/slots?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 5 min read**Forms and inputs: v-model modifiers, checkboxes and selects**](https://vuejs.school/community/blog/forms-and-inputs-v-model-modifiers-checkboxes-and-selects) [Tutorial · 6 min read**Computed properties: cache, don't recalculate**](https://vuejs.school/community/blog/computed-properties-cache-dont-recalculate)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/slots-explained-default-named-and-scoped#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-03-27","description":"Props pass data into a component, slots pass markup. Default, named and scoped slots with a Card and a List, and where v-slot is allowed to go.","name":"Slots explained: default, named and scoped","url":"https://vuejs.school/community/blog/slots-explained-default-named-and-scoped","breadcrumb":{"@id":"https://vuejs.school/community/blog/slots-explained-default-named-and-scoped#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/slots-explained-default-named-and-scoped"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/slots-explained-default-named-and-scoped#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-03-27","description":"Props pass data into a component, slots pass markup. Default, named and scoped slots with a Card and a List, and where v-slot is allowed to go.","headline":"Slots explained: default, named and scoped","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~U2xvdHMgZXhwbGFpbmVkOiBkZWZhdWx0LCBuYW1lZCBhbmQgc2NvcGVkIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~U2xvdHMgZXhwbGFpbmVkOiBkZWZhdWx0LCBuYW1lZCBhbmQgc2NvcGVk%2Cdescription_~UHJvcHMgcGFzcyBkYXRhIGludG8gYSBjb21wb25lbnQsIHNsb3RzIHBhc3MgbWFya3VwLiBEZWZhdWx0LCBuYW1lZCBhbmQgc2NvcGVkIHNsb3RzIHdpdGggYSBDYXJkIGFuZCBhIExpc3QsIGFuZCB3aGVyZSB2LXNsb3QgaXMgYWxsb3dlZCB0byBnby4%2Ceyebrow_~RGVlcCBkaXZlIMK3IDUgbWluIHJlYWQ%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9zbG90cy1leHBsYWluZWQtZGVmYXVsdC1uYW1lZC1hbmQtc2NvcGVkIg%2Cs_oc8bqQJC2vVV3thQ.png","@type":["Article","BlogPosting"],"articleSection":["Deep dive"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/slots-explained-default-named-and-scoped#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/slots-explained-default-named-and-scoped#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/slots-explained-default-named-and-scoped#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Slots explained: default, named and scoped","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~U2xvdHMgZXhwbGFpbmVkOiBkZWZhdWx0LCBuYW1lZCBhbmQgc2NvcGVkIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~U2xvdHMgZXhwbGFpbmVkOiBkZWZhdWx0LCBuYW1lZCBhbmQgc2NvcGVk%2Cdescription_~UHJvcHMgcGFzcyBkYXRhIGludG8gYSBjb21wb25lbnQsIHNsb3RzIHBhc3MgbWFya3VwLiBEZWZhdWx0LCBuYW1lZCBhbmQgc2NvcGVkIHNsb3RzIHdpdGggYSBDYXJkIGFuZCBhIExpc3QsIGFuZCB3aGVyZSB2LXNsb3QgaXMgYWxsb3dlZCB0byBnby4%2Ceyebrow_~RGVlcCBkaXZlIMK3IDUgbWluIHJlYWQ%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9zbG90cy1leHBsYWluZWQtZGVmYXVsdC1uYW1lZC1hbmQtc2NvcGVkIg%2Cs_oc8bqQJC2vVV3thQ.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~U2xvdHMgZXhwbGFpbmVkOiBkZWZhdWx0LCBuYW1lZCBhbmQgc2NvcGVkIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~U2xvdHMgZXhwbGFpbmVkOiBkZWZhdWx0LCBuYW1lZCBhbmQgc2NvcGVk%2Cdescription_~UHJvcHMgcGFzcyBkYXRhIGludG8gYSBjb21wb25lbnQsIHNsb3RzIHBhc3MgbWFya3VwLiBEZWZhdWx0LCBuYW1lZCBhbmQgc2NvcGVkIHNsb3RzIHdpdGggYSBDYXJkIGFuZCBhIExpc3QsIGFuZCB3aGVyZSB2LXNsb3QgaXMgYWxsb3dlZCB0byBnby4%2Ceyebrow_~RGVlcCBkaXZlIMK3IDUgbWluIHJlYWQ%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9zbG90cy1leHBsYWluZWQtZGVmYXVsdC1uYW1lZC1hbmQtc2NvcGVkIg%2Cs_oc8bqQJC2vVV3thQ.png"}]}
```