In Vue 3, how do you render a card's footer wrapper only when the parent passed a footer slot, and when does `$slots.footer` mislead you?
answer
- slots are functions on an object
- check presence in the template
- parent-side v-if removes the slot
- comment-only content still counts
- not reactive in script
basics
~20 sCheck the slot in Vue's template, <div v-if="$slots.footer" class="footer"><slot name="footer" /></div>, or use useSlots() in script setup. It tells you a footer fragment was passed, not that it renders anything, and it is not reactive in script.
solid answer
~40 s`$slots` (or `useSlots()` in `<script setup>`) holds each passed slot as a function returning vnodes, so `v-if="$slots.footer"` on a wrapper renders the wrapper only when the parent supplied a `#footer` fragment. It is accurate about *whether a slot function was passed*, not about whether that function renders anything. If the parent writes `<template #footer v-if="canPay">`, the compiler makes the slot itself conditional, so `$slots.footer` disappears when `canPay` is false. But if the parent writes `<template #footer><PayButton v-if="canPay" /></template>`, the slot exists and renders only a comment, so the wrapper shows up empty. Also, `slots` is not reactive: evaluating it inside the template is fine because the template re-runs on every render, but a `computed(() => !!slots.footer)` in script will not update.
code
vue · 18 lines<script setup lang="ts">
import { computed, useSlots } from 'vue'
const slots = useSlots()
// bug: slots is not reactive, so this never recomputes
const hasFooter = computed(() => !!slots.footer)
</script>
<template>
<div class="card">
<slot />
<!-- fix: check in the template, which re-runs on every render -->
<div v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</div>
</div>
</template>go deeper
Remember the v-if="$slots.name" wrapper pattern and that useSlots() gives the same object in script setup.
Explain that slots are functions keyed by name, why a v-if on the slot template removes the key, and why slots are not reactive in script.
Diagnose an empty wrapper caused by comment-only slot content and fix it at the API level, telling callers where the condition belongs.
Decide whether library components guarantee chrome-free empty regions, and document the calling convention that makes it possible.
## Slots as an object of functions Inside a Vue component, the slots its parent passed are available as an object: - `$slots` in the template (and `this.$slots` in the Options API); - `useSlots()` in `<script setup>`, which returns the same object as the `slots` field of the setup context. Each key is a slot name (`default`, `footer`, ...) and each value is a **function that returns vnodes**. A slot the parent did not pass has no key at all. That is what makes a presence check possible. ## The conditional wrapper pattern A card should not render an empty footer bar, with its padding and border, when nobody passed footer content: ```vue <!-- BaseCard.vue --> <template> <div class="card"> <div v-if="$slots.header" class="card-header"><slot name="header" /></div> <div class="card-body"><slot /></div> <div v-if="$slots.footer" class="card-footer"><slot name="footer" /></div> </div> </template> ``` This is the documented pattern for **conditional slots**. Because the check is in the template, it is re-evaluated on every render of the card. ## When `$slots.footer` misleads you The check answers one question only: did the parent pass a slot function for `footer`? Three cases matter: | Parent writes | `$slots.footer` | Wrapper | What renders inside | |---|---|---|---| | No `#footer` at all | missing | hidden | nothing | | `<template #footer v-if="canPay">` with `canPay` false | missing | hidden | nothing | | `<template #footer><PayButton v-if="canPay" /></template>` with `canPay` false | present | **shown, empty** | a comment, or the outlet's fallback if it has one | The difference comes from the compiler. A `v-if` on the slot's `<template>` makes the **slot itself** conditional, so it is added to the slots object only when the condition holds. A `v-if` inside the fragment leaves the slot function in place; calling it just yields a comment placeholder. Vue's `<slot>` outlet treats comment-only output as empty and shows its fallback, but your `v-if="$slots.footer"` wrapper has no such check. Practical fixes: 1. Ask callers to put the condition on the `<template #footer>` itself. 2. Or accept that the wrapper can be empty and style it so an empty footer collapses. 3. Or let the caller supply the footer chrome inside its fragment, so the child has no wrapper to hide. ## `slots` is not reactive The Vue docs state that the properties of `attrs` and `slots` are **not reactive**: the object is updated in place when the component re-renders, but reading it does not create a dependency. Consequences: - `v-if="$slots.footer"` in the template is correct, because the template runs again whenever the component renders; - `const hasFooter = computed(() => !!slots.footer)` in script captures the first answer and never recomputes, since nothing it read was reactive; - `const { footer } = useSlots()` destructures a stale snapshot; always read `slots.footer` at the point of use. If you need to react to slot changes in script, the docs point to `onBeforeUpdate`, which runs on every update of the component. ## Dynamic slot names The same presence check works with computed names, for example `v-if="$slots['cell-' + col.key]"` in a table that lets callers override individual columns.
- In Vue 3, why does `<template #footer v-if="canPay">` hide the card's footer wrapper when `canPay` is false, while a `v-if` inside the fragment does not?A `v-if` on the slot's `<template>` compiles into a conditional slot entry, so the `footer` key is absent from the slots object when the condition is false. A `v-if` inside the fragment keeps the slot function; it just returns a comment placeholder, so `$slots.footer` is still truthy and the wrapper renders.
- In a Vue 3 `<script setup>` component, how should you use `useSlots()` safely?Keep a reference to the whole object and read `slots.footer` at the moment you need it, typically in the template or a render function. Do not destructure it, and do not wrap a slot check in `computed`, because slot properties are not reactive and the value would go stale.
saying these in an interview costs you the question
- $slots.footer is true only when the footer content renders visible elements.
- useSlots() returns a reactive object, so computed(() => !!slots.footer) stays current.
- Destructuring footer from useSlots() once in setup is safe.
- A v-if inside a slot fragment removes the slot from $slots.
- $slots.footer holds the rendered DOM nodes of the footer.