Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 56

Flexible Layouts with Slots

Recommended resources

Hello! Welcome back to our Vue.js course.

In our last lesson, you mastered how to use v-model on your own custom components, creating a reusable BaseInput with clean two-way data binding. We saw how v-model is "syntactic sugar" for the modelValue prop and the update:modelValue event, and how the modern defineModel() macro makes this pattern incredibly simple to implement.

Today, we're exploring another fundamental concept for building flexible and reusable components: slots. While props are for passing data into a component, slots are for passing UI content—entire chunks of your template—from a parent to a child. This technique is the key to creating powerful layout components like cards, modals, and page wrappers.

This lesson directly addresses your goal of understanding industry best practices. Mastering slots will allow you to build components that are both powerful in their internal logic and flexible in their presentation, a hallmark of professional Vue development.

By the end of this lesson, you will be able to use slots to create highly flexible and composable layout components for your applications.


Part 1: What Are Slots?

At its core, a slot is a placeholder in a child component that gets filled with content from its parent. The child component defines where the content should go, but the parent component decides what that content is.

Imagine you have a component that's meant to be a styled "card" or "panel". The card component itself provides the border, background, and padding, but the content inside the card will be different every time you use it. This is the perfect use case for a slot.

In the child component, you define a slot outlet using the <slot> tag:

MyCard.vue

<template>
  <div class="card">
    <slot></slot> <!-- Parent's content will be rendered here -->
  </div>
</template>

<style>
.card {
  border: 1px solid #ccc;
  border-radius: 8px;
  padding: 16px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>

Then, in the parent component, you provide the slot content by placing it between the child component's tags:

App.vue

<template>
  <MyCard>
    <h2>This is a title</h2>
    <p>And this is some content inside the card.</p>
  </MyCard>
</template>

Vue will take the <h2> and <p> from the parent and render them where the <slot> tag was in the child.

Vue.js Slots: Content Replacement Mechanism
This diagram from the Vue documentation visually explains the core concept: content from the parent is projected into the child's template via the slot outlet.

To see this in action, the following video provides a clear and simple introduction.

Vue Slots Simplified

This video, 'Vue Slots Simplified' from LearnVue, does an excellent job of introducing the concept of slots and showing how to use a default slot with fallback content.

Watch from the beginning to 02:23. Pay attention to how the <slot> tag acts as a placeholder and how you can provide 'fallback content' that renders only when the parent doesn't provide anything.

As the video mentions, you can provide fallback content inside the <slot> tag. This content will only be rendered if the parent component provides no content for that slot.

<!-- In the child component -->
<slot>
  <p>This is the default content.</p>
</slot>


Part 2: Slots vs. Props: When to Use Which?

You now know two ways to pass things to a child component: props and slots. A common point of confusion is knowing when to choose one over the other. The distinction is crucial for writing clean and maintainable components.

  • Use Props when: You need to pass data to a component. The child uses this data to configure its own, internally-defined template. The parent passes values, but the child controls the structure.

    • Example: <Avatar user-image-url="/path/to/img.jpg" size="48" />. The Avatar component knows how to render an <img> tag; you just provide the data it needs.
  • Use Slots when: You need to pass template structure (HTML) to a component. The child provides a "frame" or "wrapper," but the parent has full control over the markup that goes inside.

    • Example: <Modal><h2>Confirm Deletion</h2><p>Are you sure?</p></Modal>. The Modal component provides the pop-up behavior and overlay, but the content inside is completely up to you.

The following video provides an excellent, concise explanation of this exact topic.

Should You Use Vue Slots Over Props?

Watch this short video 'Should You Use Vue Slots Over Props?' by John Komarnicki. It gives clear guidelines and examples that will help you decide when to use each approach.

Watch the entire video (it's short!). Focus on the distinction he makes between a component with a 'defined design' (good for props) and a component where you need 'freedom to customize the layout' (good for slots).

A common and powerful pattern is to combine props and slots. A component might take some props for basic configuration, but also include a slot for more complex or variable content. This gives you the best of both worlds: ease of use for simple cases, and flexibility for complex ones.



Part 3: Named Slots for Flexible Layouts

What if your component needs multiple, distinct areas for content? A PageLayout component, for instance, might need a header, a footer, and a main content area. A single default slot won't be enough.

This is where named slots come in. You can have as many slots as you need, as long as you give them a name attribute.

PageLayout.vue

<template>
  <div class="page-container">
    <header class="page-header">
      <slot name="header"></slot>
    </header>

    <main class="page-content">
      <slot></slot> <!-- A slot without a name is the 'default' slot -->
    </main>

    <footer class="page-footer">
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

To provide content for these named slots, the parent uses the <template> tag with the v-slot directive, or its handy shorthand, #.

App.vue

<PageLayout>
  <template v-slot:header>
    <h1>My Awesome App</h1>
  </template>
  
  <!-- You can use the # shorthand for v-slot -->
  <template #footer>
    <p>Copyright 2024</p>
  </template>
  
  <!-- Content not inside a named template goes to the default slot -->
  <p>This is the main body of the page.</p>
</PageLayout>

This pattern is extremely powerful for creating reusable layout components.

Slots - Vue.js

For a detailed textual reference, the official Vue documentation explains named slots clearly. You can also see the # shorthand in action.

Read the 'Named Slots' section. It explains the syntax for defining named slots in the child and providing content for them from the parent.



Part 4: Mini-Project: Building a Reusable AppCard

Let's apply what you've learned by creating a flexible AppCard component and using it to structure our to-do application.

Step 1: Create the AppCard.vue Component

  1. In your src/components folder, create a new file named AppCard.vue.
  2. This component will define a structure with a header, main content (default), and a footer.

A best practice when creating layout components is to only render the wrapper elements (like <header>) if the parent actually provides content for that slot. You can do this by checking Vue's built-in $slots object.

Here is the complete code for AppCard.vue:

<!-- src/components/AppCard.vue -->
<script setup>
// No script logic needed for this component
</script>

<template>
  <div class="card">
    <!-- Only render the header if the 'header' slot is used -->
    <header class="card-header" v-if="$slots.header">
      <slot name="header"></slot>
    </header>

    <main class="card-content">
      <slot></slot> <!-- Default slot for the main content -->
    </main>

    <!-- Only render the footer if the 'footer' slot is used -->
    <footer class="card-footer" v-if="$slots.footer">
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

<style scoped>
.card {
  background: white;
  border-radius: 8px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  overflow: hidden; /* Ensures child elements respect the border radius */
  margin-top: 2rem;
}

.card-header {
  padding: 1rem;
  border-bottom: 1px solid #eee;
  font-size: 1.25rem;
  font-weight: 600;
}

.card-content {
  padding: 1rem;
}

.card-footer {
  padding: 1rem;
  background-color: #f7fafc;
  border-top: 1px solid #eee;
  font-size: 0.875rem;
}
</style>

Step 2: Use AppCard in App.vue

Now, let's refactor App.vue to use our new layout component.

  1. Open src/App.vue.
  2. Import the AppCard component.
  3. Wrap your entire to-do list structure inside the <AppCard> component.
  4. Move the main heading into the header slot.
  5. Place the form for adding new to-dos and the list of to-dos into the default slot.
  6. Add a simple message in the footer slot.

Your App.vue's <script setup> section will need the import:

// src/App.vue
import AppCard from './components/AppCard.vue'; // <-- Import AppCard
// ... other imports

And your App.vue's <template> should be updated to look like this:

<!-- src/App.vue template -->
<template>
  <AppCard>
    <!-- Header Content -->
    <template #header>
      My To-Do List
    </template>

    <!-- Default Slot Content -->
    <form @submit.prevent="addTodo" class="add-todo-form">
      <BaseInput v-model="newTodoText" placeholder="Add a new task" />
      <button type="submit">Add</button>
    </form>

    <ul class="todo-list">
      <TodoItem 
        v-for="todo in todos" 
        :key="todo.id"
        :todo="todo"
        @delete-todo="deleteTodo"
      />
    </ul>
    
    <!-- Footer Content -->
    <template #footer>
      You have {{ todos.length }} task(s) in your list.
    </template>
  </AppCard>
</template>

<style>
/* Add some basic styling for the form if you haven't already */
.add-todo-form {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.add-todo-form button {
  /* ... your button styles ... */
}

.todo-list {
  list-style: none;
  padding: 0;
}
</style>

Now run your application. Your to-do list should be neatly organized inside a styled card, with a distinct header and footer. You've successfully created and used a flexible, composable layout component!



Conclusion

Fantastic work! You've just unlocked one of Vue's most powerful features for component architecture. By understanding and using slots, you can build applications that are more flexible, reusable, and easier to maintain.

Key Takeaways:

  • Slots Pass UI: Slots allow a parent component to pass template fragments (HTML and other components) into a child component.
  • Default vs. Named: A component can have one default slot and multiple named slots for creating complex layouts.
  • Syntax: Use the <slot> tag in the child and the <template #slotName> shorthand in the parent.
  • Props vs. Slots: Use props to pass data values and slots to pass layout and structure.
  • Best Practice: Use v-if="$slots.name" in layout components to avoid rendering empty wrapper elements.

In our lessons so far, we've covered the main ways components communicate: props down, events up, v-model for two-way binding, and slots for content projection. But what happens when you have a component deep inside your application that needs data from a component way at the top? Passing props through every single intermediate level can be very tedious. This problem is known as "prop drilling".

In our next lesson, we'll examine the problem of prop drilling more closely and set the stage for learning about the modern solution in Vue: centralized state management.