Why does a Vue 3 custom directive that sets an attribute in its mounted hook leave no trace in server-rendered HTML, and what does getSSRProps do?
answer
- directive hooks need a real element
- ignored during SSR
- object directive extra hook
- returns props to merge
basics
~20 sCustom directive hooks such as mounted work on a live DOM element, and the server renders strings, so Vue ignores them during SSR. An object directive's getSSRProps(binding) returns props that the server renderer merges into that element's rendered attributes.
solid answer
~40 sIn Vue 3 a custom directive's hooks (`created`, `beforeMount`, `mounted`, `updated` and the rest) receive the real element as `el`, and the server has no elements, only an HTML string, so the SSR guide says custom directives are ignored during SSR. An attribute that `mounted` sets therefore only appears after hydration. To render it on the server, add `getSSRProps(binding)` to an object directive: it returns an object of props such as `{ id: binding.value }`, and the server renderer merges it with the element's own attributes. It only receives the binding (value, arg, modifiers, instance), so it must be a pure function with no DOM or `window` access, and a function-shorthand directive has nowhere to declare it. Keep it consistent with what `mounted` sets on the client.
code
vue · 16 lines<script setup lang="ts">
import type { Directive } from 'vue'
const vTooltip: Directive<HTMLElement, string> = {
mounted(el, binding) {
el.setAttribute('title', binding.value)
},
getSSRProps(binding) {
return { title: binding.value }
}
}
</script>
<template>
<button class="icon" v-tooltip="'Delete item'">x</button>
</template>go deeper
Remember that custom directive hooks never run on the server, so anything they set is missing from the server HTML.
Explain getSSRProps: an extra member of object directives that returns props from the binding, merged into the element's server-rendered attributes.
Decide which directives contribute visible or accessible attributes that belong in the first paint, and keep their getSSRProps pure and consistent with the client hooks.
For a shared directive library used in SSR apps, set the convention that every attribute-setting directive ships a matching getSSRProps and is tested on both renderers.
## Why custom directives are silent on the server A Vue 3 **custom directive** is an object of hooks (`created`, `beforeMount`, `mounted`, `beforeUpdate`, `updated`, `beforeUnmount`, `unmounted`) that receive the element as `el` and a **binding** describing `v-name:arg.modifiers="value"`. Most directives exist to touch the DOM directly: set an attribute, focus an input, attach an observer. During SSR there is no DOM. The server renderer produces a string, so there is no `el` to pass. The Vue SSR guide therefore states that custom directives are **ignored** during SSR. A directive that sets `id`, `aria-label` or `title` in `mounted` produces HTML without that attribute; it appears only after the page hydrates in the browser and the client-side `mounted` hook runs. ## What getSSRProps is `getSSRProps` is one more optional member of an **object directive**, typed as `getSSRProps(binding, vnode)`: - It returns an object of **props** for the element, or nothing. - The server renderer **merges** the result with the element's own attributes, so a returned `class` or `style` is combined with a static one rather than replacing it. - In compiled templates it is called with the binding only (value, arg, modifiers and the component instance); `oldValue` is `undefined` and no vnode is passed, which is why the guide says it only receives the directive binding. - For an element with no children in the template, a returned `textContent` or `innerHTML` is used as the element's content in the server output. ## Writing one 1. Keep the client behaviour in the DOM hooks. 2. Add `getSSRProps` returning the attributes those hooks would set. 3. Make it a pure function of the binding: no `window`, no `document`, no element. ```ts import type { Directive } from 'vue' export const vTooltip: Directive<HTMLElement, string> = { mounted(el, binding) { el.setAttribute('title', binding.value) // client: direct DOM update }, updated(el, binding) { el.setAttribute('title', binding.value) }, getSSRProps(binding) { return { title: binding.value } // server: attributes to render } } ``` ## Which parts run where | Directive member | Runs on the server | Role | |---|---|---| | `created`, `beforeMount`, `mounted` | no | DOM work in the browser, including during hydration | | `beforeUpdate`, `updated` | no | react to later changes in the browser | | `beforeUnmount`, `unmounted` | no | teardown in the browser | | `getSSRProps` | yes | props rendered into the server HTML | ## Rules of thumb - **Use the object form.** The function shorthand only defines the `mounted`/`updated` behaviour, and the server renderer skips function directives when looking for `getSSRProps`. - **Mirror the client.** Return the same attributes the client hooks will set, so the first paint already matches the hydrated page. - **Leave behaviour client-only.** Listeners, focus management and observers have no server equivalent; do not try to express them as props. - **Built-ins do the same.** Vue's own `v-show` and `v-model` define `getSSRProps` for hand-written render functions rendered on the server; in compiled templates the SSR compiler handles those two directly. ## A worked comparison For `<button class="icon" v-tooltip="'Delete item'">x</button>`: 1. Without `getSSRProps`, the server sends the button with only `class="icon"`; the `title` exists once hydration runs `mounted`. 2. With `getSSRProps` returning `{ title: binding.value }`, the server sends the button with both `class="icon"` and `title="Delete item"`. 3. In both cases the browser still runs the directive's `created`, `beforeMount` and `mounted` hooks during hydration, so client behaviour is unchanged. The difference is visible to anything that reads the first HTML before scripts run: crawlers, assistive technology on a slow connection, and users who interact before hydration completes. ## When it matters Most directives never need it: an autofocus or click-outside directive has nothing meaningful to render on the server. `getSSRProps` earns its place when the directive contributes something visible or semantic that should be in the initial HTML, such as `title`, `aria-*` attributes, an `id`, or a class that affects layout.
- Can you add getSSRProps to a Vue 3 directive written in the function shorthand?No. The shorthand is a single function used for `mounted` and `updated`, and when the server renderer looks for `getSSRProps` it skips directives that are functions. Convert it to an object directive with `mounted` and `updated` hooks plus `getSSRProps`.
- Should a Vue getSSRProps implementation read the element's computed size to decide an attribute?It cannot. On the server there is no element and no layout, and the hook receives only the binding. Anything that depends on measuring the DOM must stay in client hooks such as `mounted`; `getSSRProps` should return only what can be derived from the binding's value, argument and modifiers.
saying these in an interview costs you the question
- Vue runs a directive's mounted hook on the server against a virtual element.
- getSSRProps receives the rendered element so it can read its attributes.
- getSSRProps replaces the element's static class instead of merging with it.
- The function shorthand directive can export getSSRProps as a property.
- Custom directives need nothing extra because hydration copies their DOM changes into the HTML.