skip to content

In Vue 3, why does a template expression such as {{ window.innerWidth }} fail, and what may a template expression contain?

level: middleimportance: should knowfreq 45%

answer

  1. one expression, like after return
  2. resolved against the component
  3. a short list of globals
  4. globalProperties or script setup

basics

~20 s

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

Vue 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

for a junior

Remember that a template binding is one expression, so use ternaries, and that browser globals such as window are not reachable in it.

for a middle

Explain the lookup order, locals, component bindings, then the allowlist, and the two ways to expose something extra.

for a senior

Keep templates free of side effects and environment assumptions, and route browser state through reactive refs so SSR and tests behave.

for a principal

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.