In Vue 3, why does a template expression such as {{ window.innerWidth }} fail, and what may a template expression contain?
answer
- one expression, like after return
- resolved against the component
- a short list of globals
- globalProperties or script setup
basics
~20 sVue 3 template expressions see only the component's scope plus a restricted globals allowlist (Math, Date, JSON and similar); window is not on it, so it resolves to undefined. Each binding must be one expression, not statements.
solid answer
~40 sVue compiles each template expression to read from the **component's render context**: `<script setup>` bindings, props, and anything exposed on the instance. Identifiers that are not bound there are only left as real globals if they are on a **restricted allowlist**, which includes `Math`, `Date`, `JSON`, `Number`, `Intl`, `console` and similar built-ins. `window`, `document` or a library attached to `window` are not on it, so `window` resolves to `undefined` on the instance (with a dev warning that it was accessed during render but is not defined) and `.innerWidth` throws. Expose what you need: import or define it in `<script setup>`, or register it on `app.config.globalProperties`. Each binding must also be **one expression**: anything valid after `return`, so ternaries yes, `if` statements and `var` declarations no.
go deeper
Remember that a template binding is one expression, so use ternaries, and that browser globals such as window are not reachable in it.
Explain the lookup order, locals, component bindings, then the allowlist, and the two ways to expose something extra.
Keep templates free of side effects and environment assumptions, and route browser state through reactive refs so SSR and tests behave.
Limit app.config.globalProperties to a few documented helpers so template scope stays explicit across a large codebase.
## What a template expression is Vue lets you write JavaScript in two places in a template: inside `{{ }}` and in the value of a directive (`:title="..."`, `v-if="..."`). The compiler turns each into code inside the component's render function, evaluated against the **component instance's scope**. ## Rule 1: one expression Each binding holds **a single expression**. The docs' quick test is whether it could follow `return`. | Allowed | Not allowed | |---|---| | `{{ count + 1 }}` | `{{ var a = 1 }}` | | `{{ ok ? 'YES' : 'NO' }}` | `{{ if (ok) { return msg } }}` | | `{{ msg.split('').reverse().join('') }}` | `{{ for (const x of xs) {} }}` | | `{{ formatDate(date) }}` | multiple statements separated by `;` | The guide notes the exceptions: `v-for`, `v-on` and `v-slot` have their own value syntax. Functions called in expressions run on **every render** of the component, so they must be free of side effects. ## Rule 2: component scope plus a globals allowlist When the compiler meets an identifier, it resolves it in this order: 1. A **local** name, such as a `v-for` alias or a slot prop. 2. A **binding of the component**: a `<script setup>` variable, import, prop, or anything else on the instance's render context. 3. An **allowed global**, left as a real global reference. The allowlist in Vue 3.5 is short: `Infinity`, `undefined`, `NaN`, `isFinite`, `isNaN`, `parseFloat`, `parseInt`, the four URI encode and decode functions, `Math`, `Number`, `Date`, `Array`, `Object`, `Boolean`, `String`, `RegExp`, `Map`, `Set`, `JSON`, `Intl`, `BigInt`, `console`, `Error` and `Symbol`. Anything else, such as `window`, `document`, `localStorage`, `location` or a script-tag library on `window`, is looked up on the **component instance**, where it does not exist. In development Vue warns that the property was accessed during render but is not defined on the instance; the value is `undefined`, and reading a field on it throws. ## Making something available - **`<script setup>`**: every top-level binding, including imports, is usable in the template. `import { formatBytes } from './format'` and then `{{ formatBytes(size) }}` just works. For browser values that change, such as viewport width, keep a `ref` updated from a listener so the template re-renders. - **`app.config.globalProperties`**: registering a value there makes it resolvable in every component's template. It suits a few app-wide helpers, not per-component state. ## Why it is designed this way - Templates stay **predictable**: a template can only reach what its component exposes, plus a known set of pure built-ins. - It avoids accidental coupling to whatever happens to be on `window` in one environment and not another, such as during server-side rendering. - It is **not a security boundary**. Vue's security guide is explicit that templates must be trusted; the allowlist is about scope hygiene, not sandboxing. ## Typical interview mistakes - Expecting `{{ window.innerWidth }}` to work because `Math.max` does. - Putting an `if` statement in a mustache instead of a ternary or a computed. - Calling a function in a template that mutates state, which runs on every render. - Treating the allowlist as protection against untrusted templates.
- How do you show the viewport width reactively in a Vue 3 `<script setup>` template?Create `const width = ref(window.innerWidth)` in `<script setup>`, update it from a `resize` listener added in `onMounted` and removed in `onUnmounted`, and render `{{ width }}`. Even if `window` were reachable in the template, reading `innerWidth` directly would not re-render on resize, because it is not reactive.
- Can a Vue 3 template call `console.log` or `JSON.stringify` directly?Yes. Both `console` and `JSON` are on Vue's template globals allowlist, so `{{ JSON.stringify(obj) }}` works. Prefer a computed for anything used more than once.
saying these in an interview costs you the question
- Templates can read any browser global such as window or document.
- Template expressions can contain if statements and declarations.
- The globals allowlist makes untrusted templates safe.
- Functions called in a template run only once.
- Imports in script setup are not visible to the template.