Nuxt
Pellicule provides a Nuxt module that integrates video rendering into your Nuxt app. Add the module, create video components, and render — your full Nuxt environment is available.
Why a Nuxt Module?
Nuxt wraps Vite with layers of runtime machinery:
- Auto-imports —
ref,computed,watchwork without import statements - Module composables —
useFetch,useRoute,useRuntimeConfigare injected at build time - Generated aliases —
#components,#importsare created dynamically - Nuxt modules —
@nuxtjs/tailwindcss,@pinia/nuxt, etc. inject plugins and config
None of this exists in a static vite.config.js. Nuxt's Vite config is constructed at runtime. So instead of Pellicule creating its own server, it renders inside your running Nuxt app where all auto-imports, modules, and aliases already work.
The pellicule/nuxt module handles this by injecting a hidden /pellicule render page into your app that Pellicule's renderer navigates to.
Setup
1. Install Pellicule
npm install pellicule2. Add the Module
Add pellicule/nuxt to your nuxt.config.ts:
export default defineNuxtConfig({
modules: ['pellicule/nuxt']
})This does three things automatically:
- Adds a
/pelliculerender page (used by Pellicule's Playwright renderer) - Registers the
defineVideoConfig()macro so it compiles cleanly - Discovers video components in your
app/videos/directory
If you use ESLint, add defineVideoConfig as a global so it doesn't flag the macro as undefined:
// eslint.config.js (or .eslintrc)
globals: {
defineVideoConfig: 'readonly'
}3. Start Your Dev Server and Render
# Start your Nuxt dev server
npx nuxt dev
# In another terminal — just works
pellicule AppDemoPellicule detects nuxt.config.ts, connects to localhost:3000, and renders your video through the injected /pellicule page.
TIP
Your Nuxt dev server must be running before you run Pellicule. Pellicule doesn't start Nuxt for you — it connects to the server you already have.
Custom Server URL
If your Nuxt dev server runs on a different port, set it once in package.json:
{
"pellicule": {
"serverUrl": "http://localhost:4000"
}
}Or pass it as a CLI flag for a one-off override:
pellicule AppDemo --server-url http://localhost:4000Resolution order: CLI flags > package.json > default (http://localhost:3000).
Custom Videos Directory
By default, the module looks for video components in app/videos/. To change this, configure it in nuxt.config.ts:
export default defineNuxtConfig({
modules: ['pellicule/nuxt'],
pellicule: {
videosDir: '~/my-videos' // ~ resolves to your srcDir
}
})Project Structure
Create a videos/ directory inside app/, next to your components and pages:
my-nuxt-app/
├── app/
│ ├── components/
│ │ ├── AppHeader.vue
│ │ └── FeatureCard.vue
│ ├── pages/
│ │ └── index.vue
│ ├── composables/
│ │ └── useAnalytics.ts
│ └── videos/ ← your video components
│ ├── AppDemo.vue
│ └── FeatureShowcase.vue
├── nuxt.config.ts
└── package.jsonExample Video Component
Because Pellicule renders inside your Nuxt app, auto-imports and your project's components work:
<script setup>
import { useFrame, useVideoConfig, interpolate, Easing } from 'pellicule'
defineVideoConfig({
durationInSeconds: 5,
width: 1920,
height: 1080
})
// useFrame and useVideoConfig are from Pellicule
const frame = useFrame()
const { fps } = useVideoConfig()
// ref and computed are auto-imported by Nuxt — no import needed
const opacity = computed(() => interpolate(frame.value, [0, fps], [0, 1]))
</script>
<template>
<div class="video" :style="{ opacity }">
<!-- These are your real Nuxt app components -->
<AppHeader />
<FeatureCard title="Lightning Fast" description="Built for speed" />
</div>
</template>
<style scoped>
.video {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #0f172a;
color: white;
}
</style>AppHeader and FeatureCard are auto-imported from app/components/ — no import statement needed, just like in the rest of your Nuxt app.
Using Nuxt Composables
Your custom composables from app/composables/ work too:
<script setup>
import { useFrame, interpolate } from 'pellicule'
const frame = useFrame()
const { data } = await useFetch('/api/stats')
const counterValue = computed(() => {
const progress = interpolate(frame.value, [0, 60], [0, 1])
return Math.round(progress * (data.value?.totalUsers || 0))
})
</script>
<template>
<div class="video">
<h1>{{ counterValue }} users and counting</h1>
</div>
</template>WARNING
Be mindful of non-deterministic data. If useFetch returns different data between renders, your video won't be reproducible. For deterministic output, consider using hardcoded sample data or mocking API responses for video rendering.
Nuxt Modules
Any Nuxt module you've installed works in your video components. If you're using @nuxtjs/tailwindcss, your Tailwind classes render. If you're using @pinia/nuxt, your stores are available. The video renders inside your full Nuxt environment.
How It Works
When you run pellicule AppDemo:
- Pellicule detects
nuxt.config.tsand enters BYOS (Bring Your Own Server) mode - It constructs the URL
http://localhost:3000/pellicule?component=AppDemo&fps=30&duration=150&width=1920&height=1080 - The
/pelliculepage (injected by the module) loadsAppDemo.vuefrom yourapp/videos/directory - The page sets up Pellicule's frame injection (
useFrame,useVideoConfig) and signals readiness - Playwright screenshots each frame, advancing the frame counter between shots
- Frames are encoded to MP4 with FFmpeg