# Slots, Attributes and Provide/Inject

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Components
Source: https://www.devtutorials.ai/learn/vue/components/slots-attributes-and-provide-inject

> 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.

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 200" role="img" aria-labelledby="fig-vue-slot" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-slot">Animation: the parent's content, a paragraph about Masala Dosa, moves into the empty slot hole inside the FancyCard component</title>
  <rect x="20" y="40" width="230" height="120" rx="12" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 2 }} />
  <text x="135" y="30" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700 }}>Parent: {"<FancyCard>…</FancyCard>"}</text>
  <rect x="390" y="40" width="230" height="120" rx="12" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 2 }} />
  <text x="505" y="30" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700 }}>FancyCard</text>
  <text x="505" y="66" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>⭐ card frame ⭐</text>
  <rect x="420" y="80" width="170" height="54" rx="8" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-blue)", strokeWidth: 1.5, strokeDasharray: "6 4" }} />
  <text x="505" y="150" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--mono)", fontSize: 12 }}>{"<slot />"}</text>
  <g>
    <animateTransform attributeName="transform" type="translate" values="0 0;0 0;370 0;370 0;0 0" keyTimes="0;0.2;0.55;0.95;1" dur="6s" repeatCount="indefinite" />
    <rect x="50" y="80" width="170" height="54" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} />
    <text x="135" y="112" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>Masala Dosa ₹90</text>
  </g>
</svg>
<figcaption>The child draws the frame. The parent fills the hole.</figcaption>
</figure>

**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.

</script>

<template>
      <h3>Masala Dosa</h3>
      <p>Crispy, with two chutneys. ₹90</p>
      <p>"Best filter coffee in town!" ⭐⭐⭐⭐⭐</p>
</template>
`,
    "FancyCard.vue": `<template>
    <div class="fancy">
      <slot />
    </div>
</template>

<style scoped>
.fancy { border: 2px solid #2f9e6e; border-radius: 12px; padding: 8px 14px; margin: 8px 0; box-shadow: 0 2px 6px #0002; }
</style>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **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.

</script>

<template>
    <SubmitButton>Book table</SubmitButton>
</template>
`,
    "SubmitButton.vue": `<template>
    <button type="submit">
      <slot>Send</slot>
    </button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

### 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.

</script>

<template>
      <template #header>
        <h2>Meera's Kitchen</h2>
      </template>

      <p>Today's special: Pongal with ghee.</p>

      <template #footer>
        <small>Open 7 am to 10 pm</small>
      </template>
</template>
`,
    "PageLayout.vue": `<template>
    <header class="top"><slot name="header" /></header>
    <main><slot /></main>
    <footer class="bottom"><slot name="footer" /></footer>
</template>

<style scoped>
.top { border-bottom: 2px solid #2f9e6e; }
.bottom { border-top: 1px dashed #999; margin-top: 8px; }
</style>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

### Dynamic Slot Names

- Choose the slot from a variable: `<template #[slotName]>`.
- This is a dynamic argument, like `:[attr]` from **Templates and Directives**.

const meal = ref<string>("breakfast");
</script>

<template>
      <template #[meal]>⭐ Chef's pick goes here</template>
    <button @click="meal = meal === 'breakfast' ? 'dinner' : 'breakfast'">Move the pick</button>
</template>
`,
    "MealTabs.vue": `<template>
    <p>Breakfast: <slot name="breakfast">Idli</slot></p>
    <p>Dinner: <slot name="dinner">Biryani</slot></p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**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.

</script>

<template>
      <template #default="{ dish, index }">
        {{ index + 1 }}. <b>{{ dish.name }}</b> <i v-if="dish.veg">🌱</i> ₹{{ dish.price }}
      </template>
</template>
`,
    "DishList.vue": `<script setup lang="ts">
const dishes = [
    { id: 1, name: "Dosa", price: 90, veg: true },
    { id: 2, name: "Biryani", price: 220, veg: false },
];
</script>

<template>
    <ul>
      <li v-for="(dish, index) in dishes" :key="dish.id">
        <slot :dish="dish" :index="index">{{ dish.name }}</slot>
      </li>
    </ul>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **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**.

</script>

<template>
      <button @click="toggle">{{ on ? "Hide" : "Show" }} the dessert menu</button>
      <ul v-if="on">
        <li>Gulab Jamun</li>
        <li>Payasam</li>
      </ul>
</template>
`,
    "ToggleState.vue": `<script setup lang="ts">

const on = ref<boolean>(false);

function toggle(): void {
    on.value = !on.value;
}
</script>

<template>
    <slot :on="on" :toggle="toggle" />
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**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.

</script>

<template>
      Kitchen opens at 7 am.
      <template #note="{ urgent }">{{ urgent ? "⚠" : "ℹ" }} Closed on Mondays.</template>
    <InfoBox>No note in this box.</InfoBox>
</template>
`,
    "InfoBox.vue": `<script setup lang="ts">

defineSlots<{
    default(): unknown;
    note?(props: { urgent: boolean }): unknown;
}>();

const slots = useSlots();
console.log("Has a note:", Boolean(slots.note));
</script>

<template>
    <div class="box">
      <slot />
      <p v-if="$slots.note" class="note"><slot name="note" :urgent="true" /></p>
    </div>
</template>

<style scoped>
.box { border: 1px solid #ccc; padding: 6px 10px; margin: 6px 0; }
.note { background: #fff4d6; margin: 6px 0 0; }
</style>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
  jsFiles={{
    "InfoBox.vue": `<script setup>

const slots = useSlots();
console.log("Has a note:", Boolean(slots.note));
</script>

<template>
    <div class="box">
      <slot />
      <p v-if="$slots.note" class="note"><slot name="note" :urgent="true" /></p>
    </div>
</template>

<style scoped>
.box { border: 1px solid #ccc; padding: 6px 10px; margin: 6px 0; }
.note { background: #fff4d6; margin: 6px 0 0; }
</style>
`,
  }}
/>

`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>`.

</script>

<template>
      Order
</template>

<style>
.big { font-size: 20px; }
</style>
`,
    "BaseButton.vue": `<script setup lang="ts">
function logHtml(event: MouseEvent): void {
    console.log((event.currentTarget as HTMLElement).outerHTML);
}
</script>

<template>
    <button class="base" @click="logHtml"><slot /></button>
</template>

<style scoped>
.base { border-radius: 8px; padding: 4px 12px; }
</style>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **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.

const phone = ref<string>("");

function onInput(event: Event): void {
    phone.value = (event.target as HTMLInputElement).value;
}
</script>

<template>
    <p>Typed: {{ phone }}</p>
</template>

<style>
.wide { width: 220px; }
</style>
`,
    "LabeledInput.vue": `<script setup lang="ts">

defineOptions({ inheritAttrs: false });
defineProps({ label: { type: String, required: true } });

const attrs = useAttrs();
console.log("Attributes given:", Object.keys(attrs).join(", "));
</script>

<template>
    <label class="field">
      {{ label }}
      <input v-bind="$attrs" />
    </label>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **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.

</script>

<template>
</template>

<style>
.note { color: crimson; }
</style>
`,
    "TwoRoots.vue": `<template>
    <h4>Two roots (warning)</h4>
    <p>Where should the class go?</p>
</template>
`,
    "FixedRoots.vue": `<template>
    <h4>Two roots, fixed</h4>
    <p v-bind="$attrs">The class goes here.</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **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.

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 220" role="img" aria-labelledby="fig-vue-provide" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-provide">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</title>
  <g style={{ fontFamily: "var(--sans)", fontSize: 14, fontWeight: 700 }}>
    <text x="150" y="20" textAnchor="middle" style={{ fill: "var(--ngc-red)" }}>Prop drilling</text>
    <text x="480" y="20" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}>provide / inject</text>
  </g>
  <g style={{ fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700 }}>
    <rect x="90" y="32" width="120" height="32" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} /><text x="150" y="53" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>App</text>
    <rect x="90" y="82" width="120" height="32" rx="8" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-slate)" }} /><text x="150" y="103" textAnchor="middle" style={{ fill: "var(--ngc-slate)" }}>Menu</text>
    <rect x="90" y="132" width="120" height="32" rx="8" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-slate)" }} /><text x="150" y="153" textAnchor="middle" style={{ fill: "var(--ngc-slate)" }}>Section</text>
    <rect x="90" y="182" width="120" height="32" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }} /><text x="150" y="203" textAnchor="middle" style={{ fill: "var(--ngc-orange)" }}>DishCard</text>
    <rect x="420" y="32" width="120" height="32" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} /><text x="480" y="53" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>App</text>
    <rect x="420" y="82" width="120" height="32" rx="8" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-slate)" }} /><text x="480" y="103" textAnchor="middle" style={{ fill: "var(--ngc-slate)" }}>Menu</text>
    <rect x="420" y="132" width="120" height="32" rx="8" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-slate)" }} /><text x="480" y="153" textAnchor="middle" style={{ fill: "var(--ngc-slate)" }}>Section</text>
    <rect x="420" y="182" width="120" height="32" rx="8" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }} /><text x="480" y="203" textAnchor="middle" style={{ fill: "var(--ngc-orange)" }}>DishCard</text>
  </g>
  <circle r="7" cx="80" style={{ fill: "var(--ngc-red)" }}><animate attributeName="cy" values="48;98;98;148;148;198" keyTimes="0;0.2;0.3;0.5;0.6;0.8" dur="6s" repeatCount="indefinite" /></circle>
  <path d="M545 48 C600 80 600 170 545 198" style={{ fill: "none", stroke: "var(--ngc-green)", strokeWidth: 2, strokeDasharray: "6 4" }} />
  <circle r="7" style={{ fill: "var(--ngc-green)" }}><animateMotion dur="6s" repeatCount="indefinite" keyPoints="0;1;1" keyTimes="0;0.35;1" path="M545 48 C600 80 600 170 545 198" /></circle>
</svg>
<figcaption>Drilling stops at every level. Inject goes straight to the child that needs it.</figcaption>
</figure>

provide("currency", "₹");
provide("shopName", "Meera's Kitchen");
</script>

<template>
</template>
`,
    "MenuSection.vue": `<script setup lang="ts">
</script>

<template>
    <h3>Breakfast</h3>
</template>
`,
    "DishCard.vue": `<script setup lang="ts">

defineProps({ name: String, price: Number });

const currency = inject<string>("currency", "$");
const shop = inject<string>("shopName");
const tax = inject<number>("taxRate", 5);
</script>

<template>
    <p>{{ name }}: {{ currency }}{{ price }} at {{ shop }} (tax {{ tax }}%)</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **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.

const theme = ref<string>("light");

function toggleTheme(): void {
    theme.value = theme.value === "light" ? "dark" : "light";
}

provide("theme", { theme: readonly(theme), toggleTheme });
</script>

<template>
    <div :class="theme">
      <p>Page content in {{ theme }} mode</p>
    </div>
</template>

<style>
.dark { background: #222; color: #eee; padding: 6px; }
.light { background: #fff; color: #222; padding: 6px; }
</style>
`,
    "TopBar.vue": `<script setup lang="ts">
</script>

<template>
    <nav>Meera's Kitchen <ThemeSwitch /></nav>
</template>
`,
    "ThemeSwitch.vue": `<script setup lang="ts">

const { theme, toggleTheme } = inject("theme") as { theme: Readonly<Ref<string>>; toggleTheme: () => void };
</script>

<template>
    <button @click="toggleTheme">Switch to {{ theme === "light" ? "dark" : "light" }}</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**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**.

const apiBase = inject<string>("apiBase");
</script>

<template>
    <p>Orders are sent to: {{ apiBase }}/orders</p>
</template>
`,
  }}
/>

## 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.

const cart = ref<string[]>(["Dosa"]);
provide(cartKey, cart);
</script>

<template>
    <button @click="cart.push('Vada')">Add vada</button>
</template>
`,
    "CartBadge.vue": `<script setup lang="ts">

const cart = inject(cartKey, ref([]));
</script>

<template>
    <p>🛒 {{ cart.length }} items</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

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

<div className="tscroll">

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

</div>

## 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**
