Examples
Headless Custom UI
Build a completely custom tour UI with headless components, render props, and your own design system or CSS framework
Headless Custom UI
Build a tour with completely custom UI using headless components. Full control over every element while keeping all the positioning, state, and accessibility logic.
What You'll Build
This example creates a custom-styled tour that:
- Uses your own design system components
- Has a unique overlay effect with gradient
- Features custom animations
- Maintains all accessibility features
Complete Code
'use client';
import {
Tour,
TourStep,
useTour,
} from '@tour-kit/react';
import {
TourCardHeadless,
TourOverlayHeadless,
TourProgressHeadless,
} from '@tour-kit/react/headless';
export default function CustomTourExample() {
return (
<Tour id="custom-tour">
<TourStep
target="#feature-1"
title="Custom Design"
content="This tour uses completely custom UI components."
placement="bottom"
/>
<TourStep
target="#feature-2"
title="Your Styles"
content="Style every element exactly how you want."
placement="right"
/>
<TourStep
target="#feature-3"
title="Full Control"
content="All positioning and accessibility handled for you."
placement="left"
/>
{/* Custom overlay with render prop */}
<CustomOverlay />
{/* Custom card with render prop */}
<CustomCard />
<AppContent />
</Tour>
);
}
// Completely custom overlay with gradient effect
function CustomOverlay() {
return (
<TourOverlayHeadless
render={({ isActive, overlayStyle, cutoutStyle, targetRect }) => (
<div
style={{
...overlayStyle,
background: 'radial-gradient(circle at center, rgba(0,0,0,0) 0%, rgba(0,0,0,0.8) 100%)',
}}
className="fixed inset-0 z-40 transition-opacity duration-300"
>
{targetRect && (
<div
style={{
...cutoutStyle,
boxShadow: '0 0 0 4px rgba(59, 130, 246, 0.5), 0 0 20px rgba(59, 130, 246, 0.3)',
}}
className="rounded-lg"
/>
)}
</div>
)}
/>
);
}
// Completely custom card with render prop
function CustomCard() {
return (
<TourCardHeadless
render={({
currentStep,
currentStepIndex,
totalSteps,
isFirstStep,
isLastStep,
next,
prev,
skip,
floatingStyles,
refs,
}) => (
<div
ref={refs.setFloating}
style={floatingStyles}
className="z-50 w-80 animate-in fade-in slide-in-from-bottom-2 duration-200"
>
{/* Custom card design */}
<div className="bg-gradient-to-br from-slate-900 to-slate-800 text-white rounded-2xl shadow-2xl border border-slate-700 overflow-hidden">
{/* Header with step indicator */}
<div className="px-5 py-4 border-b border-slate-700 flex items-center justify-between">
<span className="text-sm font-medium text-blue-400">
Step {currentStepIndex + 1} of {totalSteps}
</span>
<button
onClick={skip}
className="text-slate-400 hover:text-white transition-colors text-sm"
>
Skip
</button>
</div>
{/* Content */}
<div className="p-5 space-y-3">
<h2 className="text-xl font-bold">{currentStep?.title}</h2>
<p className="text-slate-300 leading-relaxed">
{currentStep?.content}
</p>
</div>
{/* Custom progress bar */}
<div className="px-5">
<TourProgressHeadless
current={currentStepIndex + 1}
total={totalSteps}
render={({ current, total }) => (
<div className="h-1 bg-slate-700 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 transition-all duration-300"
style={{ width: `${(current / total) * 100}%` }}
/>
</div>
)}
/>
</div>
{/* Navigation */}
<div className="p-5 flex justify-between items-center">
{!isFirstStep ? (
<button
onClick={prev}
className="px-4 py-2 text-sm font-medium text-slate-300 hover:text-white transition-colors"
>
← Back
</button>
) : (
<div />
)}
<button
onClick={next}
className="px-6 py-2.5 bg-blue-600 hover:bg-blue-500 text-white font-medium rounded-lg transition-colors"
>
{isLastStep ? 'Finish' : 'Continue →'}
</button>
</div>
</div>
</div>
)}
/>
);
}
function AppContent() {
const { start, isActive } = useTour();
return (
<div className="min-h-screen bg-slate-950 text-white p-8">
<div className="max-w-4xl mx-auto space-y-8">
<div id="feature-1" className="p-8 bg-slate-900 rounded-xl border border-slate-800">
<h2 className="text-2xl font-bold mb-2">Feature One</h2>
<p className="text-slate-400">Description of feature one</p>
</div>
<div id="feature-2" className="p-8 bg-slate-900 rounded-xl border border-slate-800">
<h2 className="text-2xl font-bold mb-2">Feature Two</h2>
<p className="text-slate-400">Description of feature two</p>
</div>
<div id="feature-3" className="p-8 bg-slate-900 rounded-xl border border-slate-800">
<h2 className="text-2xl font-bold mb-2">Feature Three</h2>
<p className="text-slate-400">Description of feature three</p>
</div>
{!isActive && (
<button
onClick={() => start()}
className="px-6 py-3 bg-blue-600 hover:bg-blue-500 rounded-lg font-medium transition-colors"
>
Start Custom Tour
</button>
)}
</div>
</div>
);
}Headless Components Overview
TourCardHeadless
Provides all card positioning and state without any styling:
<TourCardHeadless
render={({
currentStep, // Current step data (title, content, etc.)
currentStepIndex, // 0-based index
totalSteps, // Total number of steps
isFirstStep, // Boolean
isLastStep, // Boolean
next, // Go to next step
prev, // Go to previous step
skip, // Skip/exit the tour
floatingStyles, // Position styles from floating-ui
refs, // { setFloating } ref for positioning
arrowRef, // Ref for arrow element
context, // floating-ui context for advanced use
}) => (
<div ref={refs.setFloating} style={floatingStyles}>
{/* Your custom UI */}
</div>
)}
/>TourOverlayHeadless
Provides spotlight positioning without any styling:
<TourOverlayHeadless
render={({
isActive, // Whether tour is active
overlayStyle, // Base styles for full-screen overlay
cutoutStyle, // Styles for the spotlight cutout
targetRect, // DOMRect of target element
interactive, // Whether cutout allows clicks through
}) => (
<div style={overlayStyle}>
{targetRect && <div style={cutoutStyle} />}
</div>
)}
/>TourProgressHeadless
Provides progress data for custom progress indicators:
<TourProgressHeadless
current={currentStepIndex + 1}
total={totalSteps}
render={({ current, total }) => (
<div className="progress-bar">
<div style={{ width: `${(current / total) * 100}%` }} />
</div>
)}
/>Design System Integration
With shadcn/ui
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardContent, CardFooter } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
function ShadcnCard() {
return (
<TourCardHeadless
render={({ currentStep, next, prev, skip, floatingStyles, refs, currentStepIndex, totalSteps }) => (
<Card ref={refs.setFloating} style={floatingStyles} className="w-80">
<CardHeader>
<h3 className="font-semibold">{currentStep?.title}</h3>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">{currentStep?.content}</p>
<Progress value={(currentStepIndex / totalSteps) * 100} className="mt-4" />
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="ghost" onClick={skip}>Skip</Button>
<div className="space-x-2">
<Button variant="outline" onClick={prev}>Back</Button>
<Button onClick={next}>Next</Button>
</div>
</CardFooter>
</Card>
)}
/>
);
}With Chakra UI
import { Box, Button, Heading, Text, Progress, HStack } from '@chakra-ui/react';
function ChakraCard() {
return (
<TourCardHeadless
render={({ currentStep, next, prev, skip, floatingStyles, refs, currentStepIndex, totalSteps }) => (
<Box
ref={refs.setFloating}
style={floatingStyles}
bg="white"
p={6}
borderRadius="xl"
shadow="xl"
maxW="sm"
>
<Heading size="md" mb={2}>{currentStep?.title}</Heading>
<Text color="gray.600" mb={4}>{currentStep?.content}</Text>
<Progress value={(currentStepIndex / totalSteps) * 100} mb={4} colorScheme="blue" />
<HStack justify="space-between">
<Button variant="ghost" onClick={skip}>Skip</Button>
<HStack>
<Button variant="outline" onClick={prev}>Back</Button>
<Button colorScheme="blue" onClick={next}>Next</Button>
</HStack>
</HStack>
</Box>
)}
/>
);
}Custom Animations
Add entrance/exit animations:
import { motion, AnimatePresence } from 'framer-motion';
function AnimatedCard() {
return (
<TourCardHeadless
render={({ currentStep, floatingStyles, refs, ...props }) => (
<AnimatePresence mode="wait">
<motion.div
key={currentStep?.id}
ref={refs.setFloating}
style={floatingStyles}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
className="bg-white rounded-lg shadow-xl p-6 w-80"
>
<h2 className="text-xl font-bold">{currentStep?.title}</h2>
<p>{currentStep?.content}</p>
{/* Navigation... */}
</motion.div>
</AnimatePresence>
)}
/>
);
}Custom Overlay Effects
Blur Effect
function BlurOverlay() {
return (
<TourOverlayHeadless
render={({ overlayStyle }) => (
<div
style={overlayStyle}
className="fixed inset-0 backdrop-blur-sm bg-black/30"
/>
)}
/>
);
}Colored Overlay
function ColoredOverlay() {
return (
<TourOverlayHeadless
render={({ overlayStyle, cutoutStyle, targetRect }) => (
<div
style={{
...overlayStyle,
background: 'linear-gradient(135deg, rgba(59,130,246,0.3) 0%, rgba(147,51,234,0.3) 100%)',
}}
className="fixed inset-0"
>
{targetRect && (
<div
style={cutoutStyle}
className="rounded-lg ring-4 ring-blue-500/50"
/>
)}
</div>
)}
/>
);
}No Overlay (Highlight Only)
function HighlightOnly() {
return (
<TourOverlayHeadless
render={({ cutoutStyle, targetRect }) => (
<>
{targetRect && (
<div
style={{
position: 'fixed',
...cutoutStyle,
}}
className="pointer-events-none ring-4 ring-blue-500 ring-offset-4 rounded-lg animate-pulse"
/>
)}
</>
)}
/>
);
}Accessibility
Headless components maintain accessibility features:
- Focus trap: Card traps focus during tour
- ARIA attributes: Added automatically
- Keyboard navigation: Works out of the box
- Screen reader: Announcements for step changes
When using custom elements, ensure you include appropriate ARIA attributes like role="dialog" and aria-modal="true" on your card container.
Best Practices
- Keep positioning intact - Always apply
floatingStylesand userefs.setFloating - Handle all states - Check for
isFirstStep,isLastStep, etc. - Accessibility first - Include ARIA attributes and keyboard support
- Test responsively - Ensure custom designs work on mobile
Related
- Basic Tour - Using styled components
- Onboarding Flow - With persistence
- Headless Components - Full API reference