Installation
No additional dependencies are required beyond Vue. This guide covers both Vue 3 (Composition API) and Vue 2 (Options API) implementations.# Vue 3
npm install vue@^3
# Vue 2
npm install vue@^2
Vue 3 Component (Composition API)
DocumensoEmbed.vue
<template>
<iframe
:src="embedUrl"
class="documenso-embed"
allow="clipboard-write"
title="Documenso Document Signing"
/>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
interface Props {
token: string;
name?: string;
lockName?: boolean;
email?: string;
lockEmail?: boolean;
allowDocumentRejection?: boolean;
showOtherRecipientsCompletedFields?: boolean;
darkModeDisabled?: boolean;
css?: string;
cssVars?: Record<string, string>;
baseUrl?: string;
}
interface DocumentCompletedData {
token: string;
documentId: number;
envelopeId: string;
recipientId: number;
}
interface DocumentRejectedData extends DocumentCompletedData {
reason?: string;
}
interface FieldSignedData {
fieldId?: number;
value?: string;
isBase64?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
lockName: false,
lockEmail: false,
allowDocumentRejection: false,
showOtherRecipientsCompletedFields: false,
darkModeDisabled: false,
baseUrl: 'https://app.documenso.com'
});
const emit = defineEmits<{
ready: [];
complete: [data: DocumentCompletedData];
reject: [data: DocumentRejectedData];
error: [];
fieldSigned: [data: FieldSignedData];
fieldUnsigned: [data: { fieldId?: number }];
}>();
const embedUrl = computed(() => {
const embedData: Record<string, any> = {
darkModeDisabled: props.darkModeDisabled
};
if (props.name) embedData.name = props.name;
if (props.lockName) embedData.lockName = props.lockName;
if (props.email) embedData.email = props.email;
if (props.lockEmail) embedData.lockEmail = props.lockEmail;
if (props.allowDocumentRejection) {
embedData.allowDocumentRejection = props.allowDocumentRejection;
}
if (props.showOtherRecipientsCompletedFields) {
embedData.showOtherRecipientsCompletedFields = props.showOtherRecipientsCompletedFields;
}
if (props.css) embedData.css = props.css;
if (props.cssVars) embedData.cssVars = props.cssVars;
const hash = btoa(encodeURIComponent(JSON.stringify(embedData)));
return `${props.baseUrl}/embed/sign/${props.token}#${hash}`;
});
const handleMessage = (event: MessageEvent) => {
const allowedOrigin = new URL(props.baseUrl).origin;
if (event.origin !== allowedOrigin) {
return;
}
switch (event.data.action) {
case 'document-ready':
emit('ready');
break;
case 'document-completed':
emit('complete', event.data.data);
break;
case 'document-rejected':
emit('reject', event.data.data);
break;
case 'document-error':
emit('error');
break;
case 'field-signed':
emit('fieldSigned', event.data.data);
break;
case 'field-unsigned':
emit('fieldUnsigned', event.data.data);
break;
}
};
onMounted(() => {
window.addEventListener('message', handleMessage);
});
onUnmounted(() => {
window.removeEventListener('message', handleMessage);
});
</script>
<style scoped>
.documenso-embed {
width: 100%;
height: 100%;
border: none;
}
</style>
Vue 2 Component (Options API)
DocumensoEmbed.vue (Vue 2)
<template>
<iframe
:src="embedUrl"
class="documenso-embed"
allow="clipboard-write"
title="Documenso Document Signing"
/>
</template>
<script>
export default {
name: 'DocumensoEmbed',
props: {
token: {
type: String,
required: true
},
name: {
type: String,
default: undefined
},
lockName: {
type: Boolean,
default: false
},
email: {
type: String,
default: undefined
},
lockEmail: {
type: Boolean,
default: false
},
allowDocumentRejection: {
type: Boolean,
default: false
},
showOtherRecipientsCompletedFields: {
type: Boolean,
default: false
},
darkModeDisabled: {
type: Boolean,
default: false
},
css: {
type: String,
default: undefined
},
cssVars: {
type: Object,
default: undefined
},
baseUrl: {
type: String,
default: 'https://app.documenso.com'
}
},
computed: {
embedUrl() {
const embedData = {
darkModeDisabled: this.darkModeDisabled
};
if (this.name) embedData.name = this.name;
if (this.lockName) embedData.lockName = this.lockName;
if (this.email) embedData.email = this.email;
if (this.lockEmail) embedData.lockEmail = this.lockEmail;
if (this.allowDocumentRejection) {
embedData.allowDocumentRejection = this.allowDocumentRejection;
}
if (this.showOtherRecipientsCompletedFields) {
embedData.showOtherRecipientsCompletedFields = this.showOtherRecipientsCompletedFields;
}
if (this.css) embedData.css = this.css;
if (this.cssVars) embedData.cssVars = this.cssVars;
const hash = btoa(encodeURIComponent(JSON.stringify(embedData)));
return `${this.baseUrl}/embed/sign/${this.token}#${hash}`;
}
},
mounted() {
window.addEventListener('message', this.handleMessage);
},
beforeDestroy() {
window.removeEventListener('message', this.handleMessage);
},
methods: {
handleMessage(event) {
const allowedOrigin = new URL(this.baseUrl).origin;
if (event.origin !== allowedOrigin) {
return;
}
switch (event.data.action) {
case 'document-ready':
this.$emit('ready');
break;
case 'document-completed':
this.$emit('complete', event.data.data);
break;
case 'document-rejected':
this.$emit('reject', event.data.data);
break;
case 'document-error':
this.$emit('error');
break;
case 'field-signed':
this.$emit('field-signed', event.data.data);
break;
case 'field-unsigned':
this.$emit('field-unsigned', event.data.data);
break;
}
}
}
};
</script>
<style scoped>
.documenso-embed {
width: 100%;
height: 100%;
border: none;
}
</style>
Usage Examples
Basic Usage (Vue 3)
<template>
<div class="signing-page">
<h1>Sign Your Contract</h1>
<DocumensoEmbed
:token="recipientToken"
@complete="onComplete"
@error="onError"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import DocumensoEmbed from '@/components/DocumensoEmbed.vue';
const router = useRouter();
const recipientToken = ref('token-xyz-123');
const onComplete = (data: any) => {
console.log('Document signed!', data);
router.push('/success');
};
const onError = () => {
console.error('Error signing document');
router.push('/error');
};
</script>
<style scoped>
.signing-page {
height: 100vh;
display: flex;
flex-direction: column;
}
</style>
With Loading State
<template>
<div class="signing-container">
<div v-if="isLoading" class="loader-overlay">
<div class="spinner" />
<p>Loading document...</p>
</div>
<div v-if="error" class="error-overlay">
<h2>Error</h2>
<p>{{ error }}</p>
<button @click="retry">Retry</button>
</div>
<DocumensoEmbed
:token="token"
@ready="isLoading = false"
@complete="onComplete"
@error="onError"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import DocumensoEmbed from '@/components/DocumensoEmbed.vue';
const props = defineProps<{
token: string;
}>();
const isLoading = ref(true);
const error = ref<string | null>(null);
const onComplete = (data: any) => {
console.log('Completed:', data);
};
const onError = () => {
isLoading.value = false;
error.value = 'Failed to load document. Please try again.';
};
const retry = () => {
window.location.reload();
};
</script>
<style scoped>
.signing-container {
position: relative;
height: 100vh;
}
.loader-overlay,
.error-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.95);
z-index: 10;
}
.spinner {
width: 48px;
height: 48px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
Pre-filled Customer Data
<template>
<DocumensoEmbed
:token="token"
:email="customerEmail"
:lock-email="true"
:name="customerName"
:lock-name="false"
:allow-document-rejection="true"
@complete="handleComplete"
@reject="handleReject"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import DocumensoEmbed from '@/components/DocumensoEmbed.vue';
const props = defineProps<{
token: string;
customerEmail: string;
customerName: string;
}>();
const handleComplete = async (data: any) => {
// Save to backend
await fetch('/api/documents/complete', {
method: 'POST',
body: JSON.stringify(data)
});
};
const handleReject = (data: any) => {
console.log('Document rejected:', data);
};
</script>
Custom Styling
<template>
<DocumensoEmbed
:token="token"
:css-vars="brandColors"
:css="customCss"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import DocumensoEmbed from '@/components/DocumensoEmbed.vue';
const token = ref('token-123');
const brandColors = {
primary: '#3b82f6',
background: '#ffffff',
foreground: '#0f172a',
radius: '0.75rem'
};
const customCss = `
.embed--Root {
font-family: 'Inter', sans-serif;
}
button {
font-weight: 600;
}
`;
</script>
Direct Template Component
<template>
<iframe
:src="embedUrl"
class="direct-template-embed"
allow="clipboard-write"
title="Sign Document"
/>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue';
interface Props {
directToken: string;
email?: string;
lockEmail?: boolean;
name?: string;
lockName?: boolean;
externalId?: string;
cssVars?: Record<string, string>;
css?: string;
baseUrl?: string;
}
const props = withDefaults(defineProps<Props>(), {
lockEmail: false,
lockName: false,
baseUrl: 'https://app.documenso.com'
});
const emit = defineEmits<{
ready: [];
complete: [data: any];
error: [error: string];
}>();
const embedUrl = computed(() => {
const embedData: Record<string, any> = {};
if (props.email) embedData.email = props.email;
if (props.lockEmail) embedData.lockEmail = props.lockEmail;
if (props.name) embedData.name = props.name;
if (props.lockName) embedData.lockName = props.lockName;
if (props.css) embedData.css = props.css;
if (props.cssVars) embedData.cssVars = props.cssVars;
const hash = btoa(encodeURIComponent(JSON.stringify(embedData)));
const queryParams = props.externalId
? `?externalId=${encodeURIComponent(props.externalId)}`
: '';
return `${props.baseUrl}/d/${props.directToken}${queryParams}#${hash}`;
});
const handleMessage = (event: MessageEvent) => {
const allowedOrigin = new URL(props.baseUrl).origin;
if (event.origin !== allowedOrigin) return;
switch (event.data.action) {
case 'document-ready':
emit('ready');
break;
case 'document-completed':
emit('complete', event.data.data);
break;
case 'document-error':
emit('error', event.data.data);
break;
}
};
onMounted(() => {
window.addEventListener('message', handleMessage);
});
onUnmounted(() => {
window.removeEventListener('message', handleMessage);
});
</script>
<style scoped>
.direct-template-embed {
width: 100%;
height: 100%;
border: none;
}
</style>
Composable for Embed State
// composables/useDocumensoEmbed.ts
import { ref, onMounted, onUnmounted } from 'vue';
export function useDocumensoEmbed(baseUrl = 'https://app.documenso.com') {
const isReady = ref(false);
const isCompleted = ref(false);
const error = ref<string | null>(null);
const completionData = ref<any>(null);
const handleMessage = (event: MessageEvent) => {
const allowedOrigin = new URL(baseUrl).origin;
if (event.origin !== allowedOrigin) return;
switch (event.data.action) {
case 'document-ready':
isReady.value = true;
break;
case 'document-completed':
isCompleted.value = true;
completionData.value = event.data.data;
break;
case 'document-error':
error.value = 'An error occurred while loading the document';
break;
}
};
onMounted(() => {
window.addEventListener('message', handleMessage);
});
onUnmounted(() => {
window.removeEventListener('message', handleMessage);
});
const reset = () => {
isReady.value = false;
isCompleted.value = false;
error.value = null;
completionData.value = null;
};
return {
isReady,
isCompleted,
error,
completionData,
reset
};
}
Using the Composable
<template>
<div>
<div v-if="error">
Error: {{ error }}
</div>
<div v-else-if="isCompleted">
<h2>Document Signed!</h2>
<p>Document ID: {{ completionData.documentId }}</p>
</div>
<div v-else>
<div v-if="!isReady">Loading...</div>
<DocumensoEmbed :token="token" />
</div>
</div>
</template>
<script setup lang="ts">
import { useDocumensoEmbed } from '@/composables/useDocumensoEmbed';
import DocumensoEmbed from '@/components/DocumensoEmbed.vue';
const props = defineProps<{
token: string;
}>();
const { isReady, isCompleted, error, completionData } = useDocumensoEmbed();
</script>
Nuxt 3 Integration
<!-- pages/sign/[token].vue -->
<template>
<div class="container">
<DocumensoEmbed
v-if="token"
:token="token"
@complete="handleComplete"
/>
</div>
</template>
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
const route = useRoute();
const router = useRouter();
const token = computed(() => route.params.token as string);
const handleComplete = async (data: any) => {
// Call API route
await $fetch('/api/documents/complete', {
method: 'POST',
body: data
});
router.push('/success');
};
</script>
<style scoped>
.container {
height: 100vh;
}
</style>
Pinia Store Integration
// stores/document.ts
import { defineStore } from 'pinia';
export const useDocumentStore = defineStore('document', {
state: () => ({
currentToken: null as string | null,
isReady: false,
isCompleted: false,
completionData: null as any
}),
actions: {
setToken(token: string) {
this.currentToken = token;
},
markReady() {
this.isReady = true;
},
markCompleted(data: any) {
this.isCompleted = true;
this.completionData = data;
},
reset() {
this.isReady = false;
this.isCompleted = false;
this.completionData = null;
}
}
});
Using with Store
<template>
<DocumensoEmbed
v-if="documentStore.currentToken"
:token="documentStore.currentToken"
@ready="documentStore.markReady()"
@complete="documentStore.markCompleted($event)"
/>
</template>
<script setup lang="ts">
import { useDocumentStore } from '@/stores/document';
import DocumensoEmbed from '@/components/DocumensoEmbed.vue';
const documentStore = useDocumentStore();
</script>
Testing
// DocumensoEmbed.spec.ts
import { mount } from '@vue/test-utils';
import DocumensoEmbed from '@/components/DocumensoEmbed.vue';
describe('DocumensoEmbed', () => {
it('renders iframe with correct src', () => {
const wrapper = mount(DocumensoEmbed, {
props: {
token: 'test-token'
}
});
const iframe = wrapper.find('iframe');
expect(iframe.exists()).toBe(true);
expect(iframe.attributes('src')).toContain('test-token');
});
it('emits ready event when document is ready', async () => {
const wrapper = mount(DocumensoEmbed, {
props: {
token: 'test-token'
}
});
// Simulate postMessage
window.postMessage(
{ action: 'document-ready', data: null },
window.location.origin
);
await wrapper.vm.$nextTick();
expect(wrapper.emitted('ready')).toBeTruthy();
});
});
Best Practices
- Always validate message origins for security
- Use TypeScript for type safety with Composition API
- Clean up event listeners in onUnmounted/beforeDestroy
- Use computed properties for reactive embed URLs
- Handle loading and error states gracefully
- Emit typed events for better developer experience
- Use composables for reusable embed logic
Next Steps
CSS Variables
Customize appearance with CSS variables
Direct Links
Learn about direct signing links
