Skip to main content

Rich content and images

Put images, video, live state, and application components inside a tour step.

Add rich content and images

A step accepts text or a Vue component. Use a component for images, video, links, live state, or product-specific layout. Nuxt Tour keeps ownership of the card, positioning, focus, and actions.

Create the content component

vue
<!-- app/components/tours/ProjectOverview.vue -->
<template>
  <figure class="tour-media">
    <img
      src="/images/project-overview.webp"
      alt="Project overview with three active work areas"
      width="640"
      height="360"
    />
    <figcaption>
      Review progress and open work from one place.
    </figcaption>
  </figure>
</template>

<style scoped>
.tour-media {
  display: grid;
  gap: 0.75rem;
  margin: 0;
}

.tour-media img {
  width: 100%;
  height: auto;
  border-radius: 0.75rem;
}
</style>

Always set the image dimensions. This prevents layout movement when the image loads. Use useful alternative text when the image contains information. Use an empty alt value when the image is decorative.

Use the component in a definition

ts
// app/tours/onboarding.ts
import ProjectOverview from '~/components/tours/ProjectOverview.vue'

export default defineTour({
  id: 'onboarding',
  steps: [{
    id: 'overview',
    target: 'project-overview',
    title: 'Your project overview',
    content: ProjectOverview,
    placement: 'bottom',
  }],
})

The component can use stores, composables, translations, and normal Vue state. Do not pass raw HTML strings. A component keeps rendering typed and safe.

Use live application state

vue
<script setup lang="ts">
const project = useCurrentProject()
</script>

<template>
  <p>
    {{ project.completedTasks }} of {{ project.totalTasks }} tasks are complete.
  </p>
</template>

Keep the content focused. A tour card is a short guide, not a replacement for a product page.

Load large content when the step opens

Tour discovery loads definitions eagerly. Keep large content components in separate chunks with Vue's defineAsyncComponent. This keeps typed tour IDs and synchronous progress totals without a second tour-loading API.

ts
import { defineAsyncComponent, defineComponent, h } from 'vue'

const Report = defineAsyncComponent({
  loader: () => import('~/components/tours/Report.vue'),
  loadingComponent: defineComponent(() => () => h('p', 'Loading the report…')),
  errorComponent: defineComponent(() => () => h('p', { role: 'alert' }, 'The report could not load. Close the tour and try again.')),
  delay: 150,
  timeout: 10000,
})

export default defineTour({
  id: 'report',
  steps: [{ id: 'overview', title: 'Your report', content: Report }],
})

Create app/components/tours/Report.vue for this Nuxt example. In Vue, use a relative component import and import defineTour from @lupinum/nuxt-tour/vue. Install the definition through createTourPlugin.

Component loading is separate from step preparation: the card can open with its loading content. The host repositions when the content changes size. If an operation must finish before a target can be resolved, use prepare() instead. The application owns content loading errors and retry behavior.