Slots, Attributes and Provide/Inject
By Syncfusion®, Inc. · Last updated
Default, fallback, named, dynamic and scoped slots, renderless components, $slots, useSlots() and defineSlots(), then fallthrough attributes, inheritAttrs: false, $attrs and useAttrs(), many roots, provide() and inject(), reactive provided values, app-level provide and InjectionKey.
Slots
- Props pass data into a component.
- Sometimes you want to pass template content instead: text, HTML or other components.
- A slot = a hole in a child component that the parent fills with content.
- The child writes
<slot />. The parent's content appears there.
Animation: the parent's content, a paragraph about Masala Dosa, moves into the empty slot hole inside the FancyCard component
Parent:
FancyCard ⭐ card frame ⭐
Masala Dosa ₹90
The child draws the frame. The parent fills the hole.
Simple lesson: a slot is a hole in a component that the parent fills.
A Real-World Example
- Meera wants every box on her page to look the same: border, shadow and padding.
- But each box holds different content: a dish, a review, an offer.
- One
FancyCardcomponent with a slot does this.
- What happened: both cards share the same frame.
- Each card shows the content written between its tags.
What You Will Learn
- How to pass content with default, named and dynamic slots, with fallback content.
- How a child passes data to slot content: scoped slots.
- How renderless components work.
- How to check which slots the parent gave, and type them with
defineSlots(). - What fallthrough attributes are, and how
classandstylemerge. - How to place attributes yourself with
inheritAttrs: falseand$attrs. - How
provide()andinject()skip the components in the middle. - How to provide reactive values safely, and at app level.
- How to use Symbol keys and
InjectionKeyin TypeScript.
Default Slot and Fallback Content
<slot />with no name is the default slot.- Content between
<slot>and</slot>is fallback content. - Fallback content shows only when the parent gives nothing.
What Slot Content Can See
- Slot content is written in the parent, so it sees the parent's state.
- It cannot see the child's state, unless the child passes it. See scoped slots below.
Simple lesson: a default slot takes the content. Fallback content fills in when there is none.
Slot content belongs to the parent. Its classes, state and event handlers come from the parent, even though it appears inside the child.
Named Slots
- A component can have many slots. Give each one a
name. - The parent fills a named slot with
<template v-slot:name>. - The short form is
#name:<template #header>. - Content outside any
<template #...>goes to the default slot.
Dynamic Slot Names
- Choose the slot from a variable:
<template #[slotName]>. - This is a dynamic argument, like
:[attr]from Templates and Directives.
Simple lesson: name slots with name, fill them with #name.
Scoped Slots
- Sometimes the slot content needs data that lives in the child.
- The child passes it as attributes on
<slot>:<slot :dish="dish" />. - These are slot props.
- The parent receives them:
<template #default="{ dish }">. - The child decides what data to give. The parent decides how it looks.
- What happened:
DishListowns the data and the loop. - The parent decided how each row looks, using the
dishandindexslot props.
For only a default slot, you can write the slot props on the component tag itself: <DishList v-slot="{ dish }">. With named slots, always use <template #name="...">.
Renderless Components
- A renderless component = a component with logic but no look of its own.
- It keeps state and passes it out through a scoped slot.
- The parent draws everything.
- Today, a composable often does this job more simply. See Composables, Directives and Plugins.
Simple lesson: scoped slots send the child's data to the parent's content.
Conditional Slots
- In the template,
$slots.nametells you if the parent filled that slot. - Use it to skip a wrapper when the slot is empty.
- In the script,
useSlots()gives the same object. - In TypeScript,
defineSlots()(Vue 3.3) types each slot and its props.
defineSlots() only adds types for your editor and vue-tsc. It changes nothing while the app runs, so the JavaScript version simply leaves it out.
Simple lesson: check $slots before drawing a wrapper. Type slots with defineSlots().
Two Ways Data Reaches a Component
- Props and events are the main road. You learned them in Props and Events.
- But some data arrives without being declared as a prop. These are fallthrough attributes.
- And some data must travel many levels down. Passing it through every level is tiring.
provide()andinject()let a far-away child get data straight from an ancestor.
Simple lesson: attributes fall through by themselves. Provide/inject skips the middle.
In Practice: Attributes Falling Through
- Meera has a
BaseButtoncomponent used all over the site. - Sometimes she adds an
id, atitleor an extra class to it. - She did not list those as props. Vue still puts them on the real
<button>.
- What happened: the button got
id,titleand both classes. - The parent's
@clickalso reached the button. Both click handlers ran.
Fallthrough Attributes
- A fallthrough attribute = an attribute or listener passed to a component that is not a declared prop or emit.
- Common ones:
class,style,id,title,aria-*,data-*and listeners like@click. - If the component has one root element, Vue adds them to it.
classandstylemerge with the root's own. Other attributes replace.- A listener falls through too. If the root already listens, both run.
Simple lesson: anything not declared falls through to the single root element.
Turning Inheritance Off
- Sometimes the root is not the right place.
- Example: an input wrapped in a
<label>. Theplaceholderbelongs on the<input>, not on the label. - Write
defineOptions({ inheritAttrs: false }). - Then put
v-bind="$attrs"on the element that should get them. - In the script,
useAttrs()gives the same object.
- What happened:
placeholder,maxlength,classand theinputlistener went to the<input>. labelis a declared prop, so it is not in$attrs.
Many Root Elements
- A component can have more than one root element.
- Then Vue does not know where to put fallthrough attributes.
- It warns you, unless you bind
$attrson one of them yourself.
- What happened: the Console shows a warning for
TwoRoots. FixedRootschose the<p>, so its text turned red.
In TypeScript, $attrs is not typed. If a wrapper needs a known attribute, declare it as a prop instead.
Simple lesson: inheritAttrs: false + v-bind="$attrs" puts the attributes where you want.
Provide and Inject
- Prop drilling = passing a prop through many components that do not use it, just to reach a deep child.
provide(key, value)in an ancestor makes a value available to every descendant.inject(key)in any descendant reads it.- The components in the middle do nothing.
Animation: on the left, a prop is passed down through App, Menu and Section to reach DishCard. On the right, App provides the value and DishCard injects it directly, skipping the middle components
Prop drilling provide / inject
App Menu Section DishCard App Menu Section DishCard
Drilling stops at every level. Inject goes straight to the child that needs it.
- What happened:
MenuSectionpassed nothing, yetDishCardgot the values. - Nobody provided
taxRate, soinjectused its default, 5.
inject() with no default and no matching provide gives undefined and a warning. Always give a default when the value is optional.
Reactive Provided Values
- Provide a ref to share live data. The child sees every change.
- Keep changes in the provider. Provide an update function for children to call.
- Wrap the ref with
readonly(), so a child cannot change it by mistake.
Simple lesson: provide a readonly ref plus a function to change it.
App-Level Provide
app.provide(key, value)inmain.tsgives a value to every component in the app.- Good for settings, such as the shop's name or an API address.
- Plugins use it a lot. You will see this in Composables, Directives and Plugins.
Symbol Keys and InjectionKey
- Text keys like
"theme"can clash in a big app. - A Symbol is a key that is always unique.
- Keep the keys in one file and import them where needed.
- In TypeScript,
InjectionKey<T>tellsinjectwhat type comes back. Noasneeded.
Provide/inject is for data shared along one branch of the tree. Data shared across the whole app usually goes in a Pinia store. See State Management with Pinia.
Simple lesson: use Symbol keys in a shared file. Type them with InjectionKey.
TL;DR
<slot />= the default slot.- Named slot =
<slot name="header" />, filled with<template #header>. - Scoped slot =
<slot :dish="dish" />, received with#default="{ dish }". $slots/useSlots()= which slots the parent filled.- Fallthrough attributes = undeclared attributes and listeners, added to the single root.
inheritAttrs: false= set withdefineOptions(). Stops automatic fallthrough.provide(key, value)= offers a value to all descendants.inject(key, default)= reads it in any descendant.InjectionKey<T>= a typed Symbol key.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
No <slot /> in the child | Content between tags shows by itself | Add <slot /> where it goes | The parent's content disappears |
#header on a plain element | <div #header> works | Use <template #header> | A compile error |
| Reading child state in slot content | Slot content sees the child | Pass it as slot props | The value is undefined |
Mixing v-slot on the tag with named slots | Both styles mix | Use <template #...> for every slot | A compile error |
| Attributes landing on the wrapper | They go where they make sense | inheritAttrs: false + v-bind="$attrs" | placeholder ends up on a <label> |
Many roots, no $attrs | Vue picks the first root | Bind $attrs on one root | A warning, and the attributes are lost |
Providing ref.value | The child stays up to date | Provide the ref itself | The child keeps the first value |
| Changing injected state in a child | Any component may change it | Provide readonly() and an update function | Hard to find who changed the data |
inject without a default | It is always provided | Give a default value | undefined and a warning |
Check Yourself
Exercises
What to Remember
<slot>fallback content shows only when the slot is empty.- Fill named slots with
<template #name>. - Scoped slots pass the child's data to the parent's content.
classandstylemerge.inheritAttrs: false+$attrschooses the target.provide/injectskips the middle. Giveinjecta default.- Provide readonly refs plus update functions. Use typed Symbol keys.
Next: Lifecycle and Template Refs