Files
agent-skills/skills/vue-best-practices/reference/ssr-custom-directive-getssrprops.md
Jason Woltje f5792c40be feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories:
- anthropics/skills: 16 (docs, design, MCP, testing)
- obra/superpowers: 14 (TDD, debugging, agents, planning)
- coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth)
- better-auth/skills: 5 (auth patterns)
- vercel-labs/agent-skills: 5 (React, design, Vercel)
- antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo)
- Plus 13 individual skills from various repos

Mosaic Stack is not limited to coding — the Orchestrator and
subagents serve coding, business, design, marketing, writing,
logistics, analysis, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:27:42 -06:00

5.3 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Implement getSSRProps for Custom Directives in SSR MEDIUM Custom directives without SSR handling cause hydration mismatches or missing functionality best-practice
vue3
ssr
directives
custom-directive
server-side-rendering
nuxt

Implement getSSRProps for Custom Directives in SSR

Impact: MEDIUM - Custom directives only have access to the DOM on the client side. During SSR, the directive's mounted and updated hooks never run. If your directive sets attributes or modifies the element, you must implement getSSRProps to return equivalent attributes for server rendering.

Without getSSRProps, the server-rendered HTML won't include the directive's effects, causing hydration mismatches when the client applies the directive.

Task Checklist

  • Add getSSRProps hook to directives that modify element attributes
  • Return an object with HTML attributes to render on server
  • Test directive behavior in both SSR and client-only contexts
  • Consider using components instead of directives for complex SSR cases

Incorrect - Client-Only Directive:

// WRONG: No SSR handling - directive effects missing on server
const vTooltip = {
  mounted(el, binding) {
    el.setAttribute('data-tooltip', binding.value)
    el.setAttribute('aria-label', binding.value)
    el.classList.add('has-tooltip')
  }
}

Server renders:

<!-- Missing data-tooltip, aria-label, and has-tooltip class -->
<button>Hover me</button>

Client after hydration:

<!-- Directive applies, but causes mismatch -->
<button data-tooltip="Help text" aria-label="Help text" class="has-tooltip">
  Hover me
</button>

Correct - With getSSRProps:

// CORRECT: SSR-compatible directive
const vTooltip = {
  // Client-side implementation
  mounted(el, binding) {
    el.setAttribute('data-tooltip', binding.value)
    el.setAttribute('aria-label', binding.value)
    el.classList.add('has-tooltip')
  },

  // SSR implementation - returns attributes to render
  getSSRProps(binding) {
    return {
      'data-tooltip': binding.value,
      'aria-label': binding.value,
      class: 'has-tooltip'
    }
  }
}

Server now renders:

<button data-tooltip="Help text" aria-label="Help text" class="has-tooltip">
  Hover me
</button>

Complete SSR Directive Example

// directives/vFocus.js
export const vFocus = {
  // Client: Actually focus the element
  mounted(el, binding) {
    if (binding.value !== false) {
      el.focus()
    }
  },

  // SSR: Add autofocus attribute so browser focuses on load
  getSSRProps(binding) {
    if (binding.value !== false) {
      return { autofocus: true }
    }
    return {}
  }
}
<template>
  <input v-focus type="text" placeholder="Auto-focused input" />
</template>

<script setup>
import { vFocus } from '@/directives/vFocus'
</script>

Directive with Dynamic ID

// CORRECT: Generate consistent IDs
const vId = {
  mounted(el, binding) {
    el.id = binding.value || `el-${binding.instance?.$.uid}`
  },

  getSSRProps(binding, vnode) {
    // Use the same ID generation logic
    return {
      id: binding.value || `el-${vnode.component?.uid || 'ssr'}`
    }
  }
}

Handling Complex Directives

For directives that do more than set attributes, consider:

// Directive that only makes sense on client (e.g., drag-and-drop)
const vDraggable = {
  mounted(el, binding) {
    // Complex client-side logic
    initDragAndDrop(el, binding.value)
  },

  unmounted(el) {
    destroyDragAndDrop(el)
  },

  // SSR: Just mark element as draggable for styling/semantics
  getSSRProps(binding) {
    return {
      draggable: 'true',
      'data-draggable': '',
      role: 'listitem'
    }
  }
}

Directives That Cannot Have SSR Equivalents

Some directives have no meaningful server-side representation:

// Directive that tracks mouse position - no SSR equivalent
const vMousePosition = {
  mounted(el, binding) {
    el.addEventListener('mousemove', (e) => {
      binding.value?.(e.clientX, e.clientY)
    })
  },

  // Nothing meaningful to render on server
  getSSRProps() {
    return {} // Empty object - no attributes
  }
}

Nuxt.js Directive Registration

// plugins/directives.ts
export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.directive('tooltip', {
    mounted(el, binding) {
      el.setAttribute('data-tooltip', binding.value)
    },
    getSSRProps(binding) {
      return { 'data-tooltip': binding.value }
    }
  })
})

Testing SSR Directives

import { renderToString } from 'vue/server-renderer'
import { createSSRApp, h } from 'vue'
import { vTooltip } from './directives/vTooltip'

test('vTooltip renders attributes during SSR', async () => {
  const app = createSSRApp({
    directives: { tooltip: vTooltip },
    template: '<button v-tooltip="\'Help text\'">Click</button>'
  })

  const html = await renderToString(app)

  expect(html).toContain('data-tooltip="Help text"')
  expect(html).toContain('aria-label="Help text"')
})

Reference