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 FancyCard component 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

  1. How to pass content with default, named and dynamic slots, with fallback content.
  2. How a child passes data to slot content: scoped slots.
  3. How renderless components work.
  4. How to check which slots the parent gave, and type them with defineSlots().
  5. What fallthrough attributes are, and how class and style merge.
  6. How to place attributes yourself with inheritAttrs: false and $attrs.
  7. How provide() and inject() skip the components in the middle.
  8. How to provide reactive values safely, and at app level.
  9. How to use Symbol keys and InjectionKey in 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: DishList owns the data and the loop.
  • The parent decided how each row looks, using the dish and index slot 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.name tells 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() and inject() 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 BaseButton component used all over the site.
  • Sometimes she adds an id, a title or 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, title and both classes.
  • The parent's @click also 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.
  • class and style merge 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>. The placeholder belongs 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, class and the input listener went to the <input>.
  • label is 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 $attrs on one of them yourself.
  • What happened: the Console shows a warning for TwoRoots.
  • FixedRoots chose 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: MenuSection passed nothing, yet DishCard got the values.
  • Nobody provided taxRate, so inject used 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) in main.ts gives 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> tells inject what type comes back. No as needed.

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 with defineOptions(). 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

MistakeWrong ideaCorrect ideaWhat goes wrong
No <slot /> in the childContent between tags shows by itselfAdd <slot /> where it goesThe parent's content disappears
#header on a plain element<div #header> worksUse <template #header>A compile error
Reading child state in slot contentSlot content sees the childPass it as slot propsThe value is undefined
Mixing v-slot on the tag with named slotsBoth styles mixUse <template #...> for every slotA compile error
Attributes landing on the wrapperThey go where they make senseinheritAttrs: false + v-bind="$attrs"placeholder ends up on a <label>
Many roots, no $attrsVue picks the first rootBind $attrs on one rootA warning, and the attributes are lost
Providing ref.valueThe child stays up to dateProvide the ref itselfThe child keeps the first value
Changing injected state in a childAny component may change itProvide readonly() and an update functionHard to find who changed the data
inject without a defaultIt is always providedGive a default valueundefined 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.
  • class and style merge. inheritAttrs: false + $attrs chooses the target.
  • provide / inject skips the middle. Give inject a default.
  • Provide readonly refs plus update functions. Use typed Symbol keys.

Next: Lifecycle and Template Refs