---
title: "Provide and inject, explained with a theme example · vuejs.school"
description: "Pass a theme from the root of your app to a button deep inside the layout without threading a prop through every component in between."
canonical_url: "https://vuejs.school/community/blog/provide-and-inject-explained-with-a-theme"
meta:
  "og:description": "Pass a theme from the root of your app to a button deep inside the layout without threading a prop through every component in between."
  "og:image": "https://vuejs.school/_og/d/a_~UHJvdmlkZSBhbmQgaW5qZWN0LCBleHBsYWluZWQgd2l0aCBhIHRoZW1lIGV4YW1wbGUgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~UHJvdmlkZSBhbmQgaW5qZWN0LCBleHBsYWluZWQgd2l0aCBhIHRoZW1lIGV4YW1wbGU%2Cdescription_Pass%2Ba%2Btheme%2Bfrom%2Bthe%2Broot%2Bof%2Byour%2Bapp%2Bto%2Ba%2Bbutton%2Bdeep%2Binside%2Bthe%2Blayout%2Bwithout%2Bthreading%2Ba%2Bprop%2Bthrough%2Bevery%2Bcomponent%2Bin%2Bbetween.%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9wcm92aWRlLWFuZC1pbmplY3QtZXhwbGFpbmVkLXdpdGgtYS10aGVtZSI%2Cs_HRvAeTo1VTYMpqO3.png"
  "og:image:alt": "Provide and inject, explained with a theme example · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Provide and inject, explained with a theme example · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~UHJvdmlkZSBhbmQgaW5qZWN0LCBleHBsYWluZWQgd2l0aCBhIHRoZW1lIGV4YW1wbGUgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~UHJvdmlkZSBhbmQgaW5qZWN0LCBleHBsYWluZWQgd2l0aCBhIHRoZW1lIGV4YW1wbGU%2Cdescription_Pass%2Ba%2Btheme%2Bfrom%2Bthe%2Broot%2Bof%2Byour%2Bapp%2Bto%2Ba%2Bbutton%2Bdeep%2Binside%2Bthe%2Blayout%2Bwithout%2Bthreading%2Ba%2Bprop%2Bthrough%2Bevery%2Bcomponent%2Bin%2Bbetween.%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9wcm92aWRlLWFuZC1pbmplY3QtZXhwbGFpbmVkLXdpdGgtYS10aGVtZSI%2Cs_HRvAeTo1VTYMpqO3.png"
  "twitter:image:alt": "Provide and inject, explained with a theme example · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## The prop-drilling problem

The obvious way to get `theme` to the button is props. `App.vue` passes it to `AppLayout`, which passes it to `Sidebar`, which passes it to `ThemeToggle`. When the toggle emits an event, `Sidebar` and `AppLayout` each have to re-emit it upwards. Two components declare a prop and an event they never use themselves. Here is `Sidebar.vue`:

*components/Sidebar.vue**Breaks***

```
<script setup lang="ts">
import ThemeToggle from './ThemeToggle.vue'

defineProps<{ theme: 'light' | 'dark' }>()
const emit = defineEmits<{ toggle: [] }>()
</script>

<template>
  <aside>
    <nav>…</nav>
    <ThemeToggle :theme="theme" @toggle="emit('toggle')" />
  </aside>
</template>
```

The [Vue docs](https://vuejs.org/guide/components/provide-inject.html) call this prop drilling: data that has to be carried through components that only hand it on. It works, but every layer you add means two more edits, and refactoring the tree means rewiring all of them.

## Provide once, inject anywhere

Vue has a second channel between components. A parent calls `provide(key, value)`. Any component below it in the tree, however deep, calls `inject(key)` and receives that value. The components in between do nothing.

*App.vue*

```
<script setup lang="ts">
import { provide, ref } from 'vue'
import AppLayout from './components/AppLayout.vue'

const theme = ref<'light' | 'dark'>('light')

provide('theme', theme)
</script>

<template>
  <div :class="theme">
    <AppLayout />
  </div>
</template>
```

*components/ThemeToggle.vue*

```
<script setup lang="ts">
import { inject } from 'vue'

const theme = inject('theme')
</script>

<template>
  <button type="button">Theme: {{ theme }}</button>
</template>
```

`Sidebar.vue` and `AppLayout.vue` go back to having no theme prop and no event. Two rules apply:

- `provide()` and `inject()` must be called synchronously during setup. In `<script setup>`, that means at the top level, not inside a handler or a `setTimeout`.
- If two ancestors provide the same key, `inject()` gets the value from the closest one. That lets one section of the page provide its own theme for everything inside it.

## Keep the injected theme reactive

`App.vue` provides the ref itself, `theme`, not `theme.value`. Vue passes a provided ref through as it is, without unwrapping it, so the toggle holds the same ref that `App.vue` holds. When `App.vue` changes `theme.value`, the button re-renders.

This is the version that goes wrong:

*App.vue**Breaks***

```
provide('theme', theme.value) // provides the string 'light', once
```

The string is copied at the moment `provide()` runs. Later changes to the ref never reach the button, which shows `light` forever.

## Let the button change the theme safely

The button also needs to change the theme. The Vue docs recommend keeping changes to provided state inside the provider and handing out a function that makes them. In `App.vue`, provide an object with a read-only view of the ref and a function that flips it. The template stays as it was:

*App.vue*

```
<script setup lang="ts">
import { provide, readonly, ref } from 'vue'
import AppLayout from './components/AppLayout.vue'

const theme = ref<'light' | 'dark'>('light')

function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}

provide('theme', { theme: readonly(theme), toggleTheme })
</script>
```

`readonly()` returns a proxy of the ref that keeps tracking changes but refuses writes. If a descendant tries `theme.value = 'dark'`, Vue logs a warning in development and ignores the assignment. The only sanctioned way to change the theme is `toggleTheme()`, and it lives next to the state, so there is one place to look when the theme behaves strangely.

`ThemeToggle.vue` takes both from the injected object and calls `toggleTheme` on click:

*components/ThemeToggle.vue*

```
<script setup lang="ts">
import { inject } from 'vue'

const { theme, toggleTheme } = inject('theme')
</script>

<template>
  <button type="button" @click="toggleTheme">
    Switch to {{ theme === 'light' ? 'dark' : 'light' }}
  </button>
</template>
```

Destructuring is safe here because `theme` is a ref, so the reactive connection survives being pulled out of the object.

## Type the key with InjectionKey

With a string key, TypeScript cannot know what `inject('theme')` returns. It gives you `unknown`, so the destructuring above fails with `Property 'theme' does not exist on type 'unknown'`. Vue ships `InjectionKey<T>`, a `Symbol` type that carries the value type with it. Put the key and its type in `keys.ts`, next to `App.vue`, so both sides import the same key:

*keys.ts*

```
import type { InjectionKey, Ref } from 'vue'

export interface ThemeContext {
  theme: Readonly<Ref<'light' | 'dark'>>
  toggleTheme: () => void
}

export const themeKey: InjectionKey<ThemeContext> = Symbol('theme')
```

A `Symbol` key also cannot collide with a string key that another library happens to use, which matters as soon as you publish components.

In `App.vue`, import `themeKey` from `./keys` and pass it in place of the string: `provide(themeKey, { theme: readonly(theme), toggleTheme })`. TypeScript now checks the value, and an object without `toggleTheme` fails to compile.

On the other side, `inject(themeKey)` is typed as `ThemeContext | undefined`. The `undefined` is there on purpose, because nothing guarantees that an ancestor provided the key.

For an optional setting, pass a default as the second argument, `inject(key, defaultValue)`, and the `undefined` goes away. For a theme that must exist, a default would hide a missing provider, so a small composable that throws a clear error is better:

*composables/useTheme.ts*

```
import { inject } from 'vue'
import { themeKey, type ThemeContext } from '../keys'

export function useTheme(): ThemeContext {
  const context = inject(themeKey)
  if (!context) {
    throw new Error('useTheme() needs a provider. Call provide(themeKey, …) in an ancestor.')
  }
  return context
}
```

`ThemeToggle.vue` now imports `useTheme` from `../composables/useTheme` and calls `const { theme, toggleTheme } = useTheme()`, and its template does not change. It no longer mentions keys or `inject` at all. Because `useTheme()` calls `inject()`, the setup rule from earlier applies to it too: call it at the top level of `<script setup>`.

## Provide to the whole app with app.provide()

Sometimes no component sits above the one that needs the value, as with `App.vue` itself, or the value should exist before any component mounts. The application instance has `app.provide()`, which makes a value available to every component in the app. Plugins use it, because a plugin's `install(app)` function runs when `main.ts` calls `app.use()`, with no component to call `provide()` from. The same call works directly in `main.ts`:

*main.ts*

```
import { createApp, readonly, ref } from 'vue'
import App from './App.vue'
import { themeKey } from './keys'

const app = createApp(App)
const theme = ref<'light' | 'dark'>('light')

app.provide(themeKey, {
  theme: readonly(theme),
  toggleTheme: () => { theme.value = theme.value === 'light' ? 'dark' : 'light' },
})

app.mount('#app')
```

Components call `useTheme()` exactly as before, and `App.vue` can now call it too, to read the theme for its `class`. Remove the `provide()` call from `App.vue` when you move the theme here. A component that provides the same key wins inside its own subtree, so a leftover `provide()` in `App.vue` would hide the app-level theme from every component below it. The same rule still lets one section of the page override the theme for everything inside it.

## Choose between provide/inject and a store

The two tools overlap. To pick one, ask where the value belongs.

Use provide and inject for context that belongs to a part of the component tree: a form that its fields read, a tab group that its tabs register with, a theme for one section, the options of a component library. The provider is a component, the value lives exactly as long as that component does, and nothing outside the subtree can see it.

Use a Pinia store for state the whole app owns regardless of which components are mounted: the logged-in user, the cart, cached API data. A store is reachable from router guards and plain functions as well as components, and devtools shows every change to it.

A theme is a borderline case, which is why it makes a good teaching example. If only the component tree reads it, provide it from `App.vue`. If a router guard or a plain function needs it too, put it in a store and provide nothing; stores are already reachable from everywhere.

**Check it stuck**3 questions from the Provide and inject quiz.

[**Practice now**](https://vuejs.school/learn/advanced/provide-and-inject?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Release notes · 7 min read**Vue 3.6 and Vapor Mode: where things stand**](https://vuejs.school/community/blog/vue-3-6-and-vapor-mode-where-things-stand) [Deep dive · 7 min read**Vapor Mode, explained without the hype**](https://vuejs.school/community/blog/vapor-mode-explained-without-the-hype)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/provide-and-inject-explained-with-a-theme#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-06-10","description":"Pass a theme from the root of your app to a button deep inside the layout without threading a prop through every component in between.","name":"Provide and inject, explained with a theme example","url":"https://vuejs.school/community/blog/provide-and-inject-explained-with-a-theme","breadcrumb":{"@id":"https://vuejs.school/community/blog/provide-and-inject-explained-with-a-theme#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/provide-and-inject-explained-with-a-theme"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/provide-and-inject-explained-with-a-theme#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-06-10","description":"Pass a theme from the root of your app to a button deep inside the layout without threading a prop through every component in between.","headline":"Provide and inject, explained with a theme example","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~UHJvdmlkZSBhbmQgaW5qZWN0LCBleHBsYWluZWQgd2l0aCBhIHRoZW1lIGV4YW1wbGUgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~UHJvdmlkZSBhbmQgaW5qZWN0LCBleHBsYWluZWQgd2l0aCBhIHRoZW1lIGV4YW1wbGU%2Cdescription_Pass%2Ba%2Btheme%2Bfrom%2Bthe%2Broot%2Bof%2Byour%2Bapp%2Bto%2Ba%2Bbutton%2Bdeep%2Binside%2Bthe%2Blayout%2Bwithout%2Bthreading%2Ba%2Bprop%2Bthrough%2Bevery%2Bcomponent%2Bin%2Bbetween.%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9wcm92aWRlLWFuZC1pbmplY3QtZXhwbGFpbmVkLXdpdGgtYS10aGVtZSI%2Cs_HRvAeTo1VTYMpqO3.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/provide-and-inject-explained-with-a-theme#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/provide-and-inject-explained-with-a-theme#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/provide-and-inject-explained-with-a-theme#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Provide and inject, explained with a theme example","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~UHJvdmlkZSBhbmQgaW5qZWN0LCBleHBsYWluZWQgd2l0aCBhIHRoZW1lIGV4YW1wbGUgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~UHJvdmlkZSBhbmQgaW5qZWN0LCBleHBsYWluZWQgd2l0aCBhIHRoZW1lIGV4YW1wbGU%2Cdescription_Pass%2Ba%2Btheme%2Bfrom%2Bthe%2Broot%2Bof%2Byour%2Bapp%2Bto%2Ba%2Bbutton%2Bdeep%2Binside%2Bthe%2Blayout%2Bwithout%2Bthreading%2Ba%2Bprop%2Bthrough%2Bevery%2Bcomponent%2Bin%2Bbetween.%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9wcm92aWRlLWFuZC1pbmplY3QtZXhwbGFpbmVkLXdpdGgtYS10aGVtZSI%2Cs_HRvAeTo1VTYMpqO3.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~UHJvdmlkZSBhbmQgaW5qZWN0LCBleHBsYWluZWQgd2l0aCBhIHRoZW1lIGV4YW1wbGUgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~UHJvdmlkZSBhbmQgaW5qZWN0LCBleHBsYWluZWQgd2l0aCBhIHRoZW1lIGV4YW1wbGU%2Cdescription_Pass%2Ba%2Btheme%2Bfrom%2Bthe%2Broot%2Bof%2Byour%2Bapp%2Bto%2Ba%2Bbutton%2Bdeep%2Binside%2Bthe%2Blayout%2Bwithout%2Bthreading%2Ba%2Bprop%2Bthrough%2Bevery%2Bcomponent%2Bin%2Bbetween.%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy9wcm92aWRlLWFuZC1pbmplY3QtZXhwbGFpbmVkLXdpdGgtYS10aGVtZSI%2Cs_HRvAeTo1VTYMpqO3.png"}]}
```