Vue 3 Integration
Use Dyrected in a standard Vue 3 application with composables, live preview, and the Admin UI bridge.
The @dyrected/vue package provides Vue 3 composables, a React-in-Vue Admin bridge, and live preview support for any Vite-based or framework-agnostic Vue 3 app. If you are using Nuxt, use @dyrected/nuxt instead — it re-exports everything from this package and adds Nuxt-specific glue.
Installation
pnpm add @dyrected/vue @dyrected/sdkEmbedding the Admin UI
@dyrected/vue ships a <DyrectedAdmin /> component that mounts the React-based Admin UI into your Vue app using a React-in-Vue bridge:
<!-- src/pages/Admin.vue -->
<script setup lang="ts">
import { DyrectedAdmin } from '@dyrected/vue'
</script>
<template>
<DyrectedAdmin
base-url="https://api.dyrected.cloud"
api-key="sk_live_..."
/>
</template>Props
Prop
Type
Passing Vue components as custom field inputs
You can inject Vue components as custom admin field inputs. The bridge wraps them automatically so they render inside the React-based Admin UI.
Register the key in your collection schema
In your dyrected.config.ts, add admin.component to any field. The value is a string key that maps to the component you provide to <DyrectedAdmin>:
// dyrected.config.ts
import { defineConfig, defineCollection } from '@dyrected/core'
export default defineConfig({
collections: [
defineCollection({
slug: 'products',
fields: [
{ name: 'title', label: 'Title', type: 'text' },
{
name: 'brandColor',
label: 'Brand Color',
type: 'text',
admin: {
component: 'products.brandColor', // unique key — matches the prop below
description: 'Brand hex color, e.g. #6366f1',
},
},
],
}),
],
})Write your Vue component
The bridge passes the following props to your component:
Prop
Type
<!-- components/BrandColorPicker.vue -->
<script setup lang="ts">
const props = defineProps<{
value: string | null
onChange: (v: string) => void
disabled: boolean
}>()
</script>
<template>
<div class="color-picker-wrapper">
<input
type="color"
:value="value ?? '#000000'"
:disabled="disabled"
@input="(e) => onChange((e.target as HTMLInputElement).value)"
/>
<span>{{ value ?? 'None selected' }}</span>
</div>
</template>Pass the component to <DyrectedAdmin>
<!-- pages/admin.vue -->
<script setup lang="ts">
import { DyrectedAdmin } from '@dyrected/vue'
import BrandColorPicker from '~/components/BrandColorPicker.vue'
</script>
<template>
<ClientOnly>
<DyrectedAdmin
:components="{
fields: {
'products.brandColor': BrandColorPicker,
}
}"
/>
</ClientOnly>
</template>The key ('products.brandColor') must exactly match the admin.component value in your schema.
Virtual fields (computed/read-only display)
You can combine a custom component with an afterRead hook to display a virtual/computed field that is never stored in the database:
// dyrected.config.ts
{
name: 'fullName',
type: 'text',
admin: {
component: 'users.fullName',
readOnly: true,
description: 'Automatically computed from first and last name.',
},
access: { update: false }, // prevent API writes
hooks: {
afterRead: [
({ value, doc }) => `${doc.firstName ?? ''} ${doc.lastName ?? ''}`.trim() || value,
],
},
}copyable — Copy-to-clipboard fields
Add a copy button to any read-only field by wrapping value in your component:
<!-- components/CopyableField.vue -->
<script setup lang="ts">
const props = defineProps<{ value: string | null }>()
const copied = ref(false)
async function copy() {
if (!props.value) return
await navigator.clipboard.writeText(props.value)
copied.value = true
setTimeout(() => (copied.value = false), 2000)
}
</script>
<template>
<div class="copyable-field">
<span>{{ value ?? '—' }}</span>
<button type="button" @click="copy">
{{ copied ? '✓ Copied' : 'Copy' }}
</button>
</div>
</template>Then in your schema:
{
name: 'apiKey',
type: 'text',
admin: {
component: 'users.apiKey',
readOnly: true,
},
access: { update: false },
}Advanced: Custom Admin Components in Vue
When building rich custom field components in Vue 3 that run inside the React-based Admin UI:
- How the Bridge Works: The React-in-Vue bridge mounts your Vue component dynamically inside the React render tree. Properties (
value,onChange,field, etc.) are converted to reactive Vue props automatically. - Accessing the Document ID: Since the document
idis read-only metadata (not a registered form field), it is not present in standard form watch values. You can read the current document ID in two ways:- Access the prop
context.siblingData.id(which Dyrected injects automatically on Edit forms). - If the component runs on an older version or outside form context, you can parse it from
window.location.hashas a fallback.
- Access the prop
- Reacting to Router Navigation: Because the Admin UI runs on React Router, Vue's
computed()properties or route watchers (like Nuxt/Vue Router) will not trigger when the URL changes. If your custom component needs to react to URL changes, listen to the browser hashchange event directly:import { onMounted, onUnmounted } from 'vue' onMounted(() => { window.addEventListener('hashchange', handleHashChange) }) onUnmounted(() => { window.removeEventListener('hashchange', handleHashChange) }) - Bridge Context Properties:
The
contextprop passed to your custom Vue component contains useful metadata:context.user: The currently authenticated administrator user.context.schemas: The loaded Dyrected collection configuration schemas.context.siblingData: Object containing current sibling field values in the form, including the documentid.
Fetching Content — useDyrected
<script setup lang="ts">
import { useDyrected } from '@dyrected/vue'
const { doc, pending, error } = useDyrected('pages', 'home')
</script>
<template>
<div v-if="pending">Loading...</div>
<div v-else-if="error">{{ error.message }}</div>
<div v-else>
<h1>{{ doc?.title }}</h1>
</div>
</template>Live Preview — useLivePreview
The useLivePreview composable subscribes to postMessage events from the Dyrected Admin UI and updates your component data reactively.
<!-- pages/PostPreview.vue -->
<script setup lang="ts">
import { useLivePreview } from '@dyrected/vue'
const props = defineProps<{ initialPost: Post }>()
const { data: post, isLive } = useLivePreview({
initialData: props.initialPost,
serverURL: import.meta.env.VITE_CMS_ADMIN_URL,
})
</script>
<template>
<div v-if="isLive" class="preview-banner">Preview Mode</div>
<h1>{{ post?.title }}</h1>
</template>Composable API:
const { data, isLive } = useLivePreview<T>({
initialData: T, // Shown until the first postMessage arrives
serverURL?: string, // Admin origin for origin validation (defaults to '*')
})Prop
Type
Inline editing support
Add data-dy-path attributes to your template elements to enable click-to-field navigation in the Admin's Edit Mode:
<template>
<article>
<h1 data-dy-path="title">{{ post?.title }}</h1>
<p data-dy-path="excerpt">{{ post?.excerpt }}</p>
<img :src="post?.coverImage?.url" data-dy-path="coverImage" />
</article>
</template>When the editor activates Edit Mode in the preview toolbar, clicking any marked element sends the field path back to the Admin, which scrolls to and focuses the corresponding form input. The useLivePreview composable wires up all the hover and click listeners automatically.
Rendering components
@dyrected/vue ships ready-to-use components for the most common content types, so you don't have to hand-render media or icons.
<DyrectedImage>
A plain-<img> wrapper that accepts a Dyrected Media object or a URL string and handles width, height, and alt automatically.
<script setup lang="ts">
import { DyrectedImage } from '@dyrected/vue'
</script>
<template>
<DyrectedImage :media="post.coverImage" :width="800" :height="400" />
</template><DyrectedMedia>
Renders the right element based on media type — YouTube embed, image, video, or a download link fallback.
<script setup lang="ts">
import { DyrectedMedia } from '@dyrected/vue'
</script>
<template>
<DyrectedMedia :media="post.featuredMedia" :width="800" :height="450" />
</template><DyrectedIcon>
The icon field type stores the name of a Lucide icon (e.g. "ChartNoAxesCombined"), not SVG markup. <DyrectedIcon> resolves that name to the matching icon so you can render an icon field value directly.
<script setup lang="ts">
import { DyrectedIcon } from '@dyrected/vue'
</script>
<template>
<!-- feature.icon === "ChartNoAxesCombined" -->
<DyrectedIcon :name="feature.icon" class="w-6 h-6 text-primary" />
</template>Standard Lucide attributes (size, color, stroke-width, class, …) fall through to the underlying icon.
Prop
Type
Browse valid icon names at lucide.dev/icons. The Admin UI's icon-picker field uses the same set, so any value an editor can choose will resolve here. In Nuxt these render components are auto-imported (<DyrectedIcon>, <DyrectedMedia>, <DyrectedBlocks>) — no import needed.
<Blocks>
Renders an array of blocks by blockType, scoping each item's data-dy-path for click-to-edit. See Building a page builder.
Reference — everything @dyrected/vue exports
| Export | Kind | Purpose |
|---|---|---|
DyrectedAdmin | Component | Embeds the React-based Admin UI via the React-in-Vue bridge. |
DyrectedImage | Component | <img> wrapper for a Media object or URL. |
DyrectedMedia | Component | Renders image / video / YouTube / download by media type. |
DyrectedIcon | Component | Renders an icon field value as a Lucide icon. |
Blocks | Component | Renders an array of blocks by blockType to your block components. |
DyPathScope | Component | Scopes a data-dy-path base for nested live-preview editing. |
useDyrected | Composable | Fetch a document / collection / global. |
useLivePreview | Composable | Subscribe to Admin preview messages + inline editing. |
useDyrectedAuth | Composable | Admin authentication state + actions. |
useDyPath | Composable | Read the current data-dy-path scope. |
Package Structure
packages/vue/
├── src/
│ ├── components/
│ │ ├── DyrectedAdmin.vue # React-in-Vue bridge component
│ │ ├── DyrectedImage.vue # <img> wrapper for Media
│ │ ├── DyrectedMedia.vue # Type-aware media renderer
│ │ ├── DyrectedIcon.vue # Lucide icon renderer for `icon` fields
│ │ ├── Blocks.ts # Block-array renderer
│ │ └── DyPathScope.ts # data-dy-path scoping
│ ├── composables/
│ │ ├── useDyrected.ts # Content fetching
│ │ └── useLivePreview.ts # Live preview + inline editing
│ ├── bridge/
│ │ └── react-in-vue.ts # React root mounting + prop sync
│ └── index.ts