Security and Accessibility

This section documents the current Vue 3.x release line as published at the official Vue.js documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. The Composition API with <script setup> is the authoring style used throughout; the Options API is shown only as a contrast.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

Vue removes the most common cross-site-scripting hole by escaping interpolated content automatically, and its component model makes accessible markup the path of least resistance. Both still need deliberate attention at the edges. This page follows the Security and Accessibility guides.

Security

Built-in HTML escaping

Text bindings — \{\{ mustache }}, v-text, and : attribute bindings — are escaped before they reach the DOM, so user data rendered this way cannot introduce markup or script. The What Vue Does section spells this out.

<script setup lang="ts">
const comment = `<img src=x onerror="alert(document.cookie)">`
</script>

<template>
  <!-- rendered as literal text, no image element, no script -->
  <p>{{ comment }}</p>
  <p :title="comment">hover me</p>
</template>

v-html — the injection you opt into

v-html sets innerHTML verbatim and bypasses all escaping. Never point it at user-supplied or third-party content. When you must render rich text, sanitise first with a vetted library and treat its allowlist as security-critical:

<script setup lang="ts">
import DOMPurify from 'dompurify'
const props = defineProps<{ markdownHtml: string }>()
const safe = computed(() => DOMPurify.sanitize(props.markdownHtml))
</script>

<template>
  <article v-html="safe" />
</template>

URL, attribute, style, and :is injection

  • URLs — a bound :href or :src of javascript:alert(1) executes on click. Validate the scheme (https: / mailto: / relative) before binding, or run URLs through a sanitiser. The Potential Dangers section covers this.

  • Attributes — never bind user data into :srcdoc, :formaction, or event-handler-like attributes. Binding user text into an ordinary attribute such as :title is fine (it is escaped).

  • Styles — do not interpolate user input into :style strings or <style> blocks; CSS can load resources and, in old engines, run expressions. Bind individual, validated properties instead.

  • Dynamic components — <component :is="…​"> with a user-controlled value can mount an arbitrary registered component or, with a string, an arbitrary element. Resolve the value against an explicit allowlist map, never pass it straight through.

<script setup lang="ts">
const views = { chart: ChartView, table: TableView } as const
const props = defineProps<{ view: string }>()
const current = computed(() => views[props.view as keyof typeof views] ?? TableView)

function safeHref(raw: string) {
  return /^(https?:|mailto:|\/)/.test(raw) ? raw : '#'
}
</script>

<template>
  <component :is="current" />
  <a :href="safeHref(userLink)">link</a>
</template>

Template injection

Vue templates are code. Never build a template string from user input and hand it to compile(), h() with a user-derived type, or a runtime template option. Keep templates static and drive them with data.

Providing server data safely

Injecting state into the page for the client to pick up is a classic XSS sink. Serialise with a context-aware tool, not JSON.stringify alone (which leaves </script> and <!-- intact):

import serialize from 'serialize-javascript'

const html = `<script>window.__INITIAL__ = ${serialize(state, { isJSON: true })}<` + `/script>`

On the client, read it from a data-* attribute or a typed global — do not eval it.

Content Security Policy

Vue’s production runtime needs no unsafe-eval: SFCs are compiled to render functions at build time. Ship a strict policy (script-src 'self'; nonce or hash for any inline bootstrap) and keep the runtime-only Vue build so the new Function template compiler is never bundled. Avoid runtime string templates, which would require unsafe-eval.

SSR-specific concerns

  • Escape interpolations into the HTML shell (the parts outside the app’s mount point) yourself — Vue only escapes what it renders.

  • Never render request-derived HTML with v-html on the server; a reflected payload becomes stored-looking XSS on first paint.

  • Keep per-request state on a fresh app instance; a module-level singleton leaks one user’s data into another’s response.

  • Validate that hydration data matches what the client will re-render, so an attacker cannot smuggle markup through a mismatch.

For the browser mechanism that governs which origins a Vue app may call, and how to validate origins on the API, see What is CORS?.

Accessibility

Vue renders plain HTML, so accessibility is mostly a matter of authoring good templates. The Accessibility guide is the reference.

Semantic templates

Use landmark and heading elements instead of styled `<div>`s; component boundaries do not have to match element boundaries.

<template>
  <header>
    <nav aria-label="Primary"><!-- ... --></nav>
  </header>
  <main>
    <h1>{{ pageTitle }}</h1>
    <article>
      <h2>{{ section.title }}</h2>
    </article>
  </main>
  <footer><!-- ... --></footer>
</template>

Give keyboard and screen-reader users a way past repeated navigation. See Skip link.

<template>
  <a class="skip-link" href="#main">Skip to main content</a>
  <!-- ...nav... -->
  <main id="main" tabindex="-1"><RouterView /></main>
</template>

<style>
.skip-link {
  position: absolute;
  transform: translateY(-120%);
}
.skip-link:focus {
  transform: translateY(0);
}
</style>

Route-change focus management

An SPA navigation does not move focus or announce the new page. After each route change, move focus to the main region or the new <h1>:

<script setup lang="ts">
import { useRoute } from 'vue-router'
import { watch, useTemplateRef } from 'vue'

const main = useTemplateRef<HTMLElement>('main')
const route = useRoute()

watch(() => route.fullPath, () => {
  main.value?.focus()
})
</script>

<template>
  <main ref="main" tabindex="-1">
    <RouterView />
  </main>
</template>

Pair it with a visually-hidden aria-live="polite" region that you update with the new page title so the change is announced.

aria-* bindings

Bind ARIA state from reactive data; keep it in sync with what the user sees. See Semantic Forms for the form patterns.

<script setup lang="ts">
const open = ref(false)
const busy = ref(false)
</script>

<template>
  <button
    :aria-expanded="open"
    aria-controls="panel"
    @click="open = !open"
  >
    Details
  </button>
  <section id="panel" v-show="open" :aria-busy="busy">...</section>
</template>

Labelled form controls

Every control needs a programmatic name — a <label for> tied to a unique id, or aria-label / aria-labelledby. useId generates SSR-stable ids:

<script setup lang="ts">
import { useId } from 'vue'
const nameId = useId()
const name = defineModel<string>()
</script>

<template>
  <label :for="nameId">Full name</label>
  <input :id="nameId" v-model="name" type="text" autocomplete="name" />
</template>

Group related controls in a <fieldset> with a <legend>; associate error text with aria-describedby.

Testing

Automated tools catch a large share of issues — run them, then still keyboard-test and screen-reader-test by hand:

  • vue-axe — a dev-only plugin that runs axe-core against the live component tree and logs violations to the console.

  • Lighthouse — the Accessibility category in Chrome DevTools or CI (@lhci/cli) for a per-page score and specific failures.

  • @axe-core/playwright or jest-axe / vitest-axe — assert zero violations inside component and end-to-end tests.

// main.ts (development only)
if (import.meta.env.DEV) {
  const { default: VueAxe } = await import('vue-axe')
  app.use(VueAxe)
}

For the standards behind these checks — WCAG, the POUR principles, conformance levels, and the legal requirements — see Web Accessibility.

See also