Skip to main content

Customize the tour

Theme the default card or replace its content without losing accessibility.

Customize the tour

The default theme is intentionally useful without configuration. It follows the system color scheme and recognizes .light, .dark, [data-theme="light"], and [data-theme="dark"].

Theme variables

Override variables on the tour root from application CSS:

css
[data-tour-part='root'] {
  --tour-accent: #0f766e;
  --tour-accent-hover: #115e59;
  --tour-card-radius: 1rem;
  --tour-arrow-size: 1rem;
  --tour-overlay-color: rgb(2 6 23 / 64%);
  --tour-spotlight-padding: 10px;
}

Import structure.css instead of style.css when your design system owns all visual rules. Set nuxtTour.css to false when application CSS imports an entry itself.

Replace one section

Use #actions to supply application buttons while keeping the default title, content, progress, and close control. Use #progress to change only progress. Both slots receive the card context and resolved labels.

vue
<script setup lang="ts">
const error = ref('')

async function advance(next: () => Promise<void>) {
  error.value = ''
  try {
    await next()
  }
  catch (cause) {
    error.value = cause instanceof Error ? cause.message : 'The next step could not open.'
  }
}
</script>

<template>
  <p v-if="error" role="alert">{{ error }}</p>
  <TourHost>
    <template #progress="{ index, total, labels }">
      <p>{{ labels.progress(index + 1, total) }}</p>
    </template>
    <template #actions="{ controller, pending, index, total, labels }">
      <button :disabled="pending" @click="advance(controller.next)">
        {{ index === total - 1 ? labels.finish : labels.next }}
      </button>
    </template>
  </TourHost>
</template>

This example uses Nuxt auto-imports. In Vue, import ref from vue and TourHost from @lupinum/nuxt-tour/vue. Your actions control their pending presentation and error feedback. Style your own buttons; the built-in button theme applies only to default actions. The default close control and Escape remain available.

The #card slot takes precedence when supplied. Section slots are not rendered inside a custom whole card.

Replace card content

vue
<script setup lang="ts">
import { ref } from 'vue'
import { TourContent, TourHost } from '@lupinum/nuxt-tour/vue'

const error = ref('')
async function advance(next: () => Promise<void>) {
  error.value = ''
  try { await next() }
  catch (cause) {
    error.value = cause instanceof Error ? cause.message : 'The next step could not open.'
  }
}
</script>

<template>
  <p v-if="error" role="alert">{{ error }}</p>
  <TourHost>
    <template #card="{ step, title, controller, titleId, descriptionId, pending }">
      <h2 v-if="title" :id="titleId">{{ title }}</h2>
      <div :id="descriptionId"><TourContent :step="step" /></div>
      <button :disabled="pending" @click="advance(controller.next)">Continue</button>
    </template>
  </TourHost>
</template>

Import TourContent from @lupinum/nuxt-tour/vue. The host keeps ownership of the dialog wrapper, name fallback, focus scope, Escape, positioning, and cleanup. Use the supplied IDs so custom content remains correctly described.

Choose motion once

Motion is automatic by default. An eligible opening moves between nearby visible targets when the requested scroll alignment needs no movement. A real scroll or route change keeps a covered handoff. scroll: false controls scrolling only.

Disable all tour-owned animation and smooth scrolling in Nuxt:

ts
export default defineNuxtConfig({
  modules: ['@lupinum/nuxt-tour'],
  nuxtTour: { motion: 'none' },
})

In Vue, set the same option on createTourPlugin({ tours: [onboarding], motion: 'none' }). The default is motion: 'auto', which respects the operating system's reduced-motion preference. The policy does not change animations inside your own content components or elsewhere in the application.

Set clear spacing

Use gap: 16 on a step for 16 CSS pixels of clear space beyond the padded spotlight and arrow reach. The default is 12. Existing offset values retain their raw-target-to-card meaning. Use one option; specifying both is an error. Placement may change to keep the card visible, and a modal card may overlap an oversized target when the viewport leaves no external space.

Translate step names reactively

Control labels and step labels belong to the host. The optional labels.step callback reads reactive state and returns translated heading and dialog labels. The installed definition and controller's currentStep remain unchanged.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { TourHost } from '@lupinum/nuxt-tour/vue'
import type { TourLabels } from '@lupinum/nuxt-tour/vue'

const language = ref<'en' | 'de'>('en')
const labels: Partial<TourLabels> = {
  step: ({ tourId, step }) => {
    if (tourId !== 'onboarding' || step.id !== 'welcome') return {}
    return { title: language.value === 'de' ? 'Willkommen' : 'Welcome' }
  },
}
</script>

<template>
  <TourHost :labels="labels" />
</template>

In an i18n application, return values from its translation function in the same callback. Translate ariaLabel too when the definition supplies a separate accessible name. Omitted or blank translations retain the definition's validated labels. A callback error is reported through the browser's error reporting and retains those fallback labels. Custom slots receive tourId, resolved title, and resolved ariaLabel; use title for a translated custom heading.

Keep command error feedback outside the host. A fatal navigation error closes the tour before rejecting, so an error inside a card slot would disappear.