Shadcn Tour for React and Tailwind
Displays a guided tour of the application.
Install
Add the component with the CLI, then import it.
Usage
Compose steps, then call start() from a trigger.
Installation
bunx --bun shadcn@latest add https://kit.dev/r/tour.jsonpnpm dlx shadcn@latest add https://kit.dev/r/tour.jsonnpx shadcn@latest add https://kit.dev/r/tour.jsonyarn shadcn@latest add https://kit.dev/r/tour.json<Step>This component depends on Button and Dialog. Install them first if you haven't already.</Step>
Install the following dependencies:
bun add @ark-ui/react lucide-reactpnpm add @ark-ui/react lucide-reactnpm install @ark-ui/react lucide-reactyarn add @ark-ui/react lucide-reactCopy and paste the following code into your project.
"use client";
import { ark } from "@ark-ui/react/factory";
import { Portal } from "@ark-ui/react/portal";
import {
Tour as ArkTour,
type TourStepDetails,
type UseTourReturn,
useTour,
} from "@ark-ui/react/tour";
import { ChevronLeft, ChevronRight, X } from "lucide-react";
import React from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
DialogBody,
DialogFooter,
DialogHeader,
type DialogOverlay,
dialogOverlayVariants,
} from "@/components/ui/dialog";
export type TourStepType = TourStepDetails;
interface TourProviderProps {
/**
* The function to start the tour
*/
handleStart: () => void;
/**
* The tour instance
*/
tour: UseTourReturn;
}
const TourProvider = React.createContext<TourProviderProps>(
{} as TourProviderProps
);
interface TourProps
extends Omit<React.ComponentProps<typeof ArkTour.Root>, "tour"> {
/**
* Enable arrow key navigation between steps
*/
keyboardNavigation?: boolean;
/**
* Called when the tour status changes
*/
onStatusChange?: (details: { status: string }) => void;
/**
* Called when the current step changes
*/
onStepChange?: (details: { stepId: string | null }) => void;
/**
* The steps to display in the tour
*
* @default []
*/
steps: TourStepDetails[];
}
export const Tour = (props: TourProps) => {
const { steps = [], lazyMount = true, unmountOnExit = true, ...rest } = props;
const tour = useTour({ steps });
const handleStart = React.useCallback(() => {
tour.start();
}, [tour]);
return (
<TourProvider.Provider value={{ handleStart, tour }}>
<ArkTour.Root
data-slot="tour"
lazyMount={lazyMount}
tour={tour}
unmountOnExit={unmountOnExit}
{...rest}
/>
</TourProvider.Provider>
);
};
interface TourTriggerProps extends React.ComponentProps<typeof ark.button> {}
export const TourTrigger = (props: TourTriggerProps) => {
const { onClick, ...rest } = props;
const { handleStart } = useTourContext();
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
onClick?.(e);
handleStart();
};
return (
<ark.button
data-slot="tour-trigger"
type="button"
{...rest}
onClick={handleClick}
/>
);
};
export const TourActionTrigger = (
props: React.ComponentProps<typeof ArkTour.ActionTrigger>
) => <ArkTour.ActionTrigger data-slot="tour-action-trigger" {...props} />;
export const TourOverlay = (
props: React.ComponentProps<typeof DialogOverlay>
) => {
const { className, ...rest } = props;
return (
<ArkTour.Backdrop
className={cn(dialogOverlayVariants(), "duration-initial", className)}
data-slot="tour-overlay"
{...rest}
/>
);
};
export const TourPositioner = (
props: React.ComponentProps<typeof ArkTour.Positioner>
) => (
<ArkTour.Positioner
className={cn(
"z-50",
"data-[type=dialog]:fixed data-[type=dialog]:inset-0",
"data-[type=dialog]:flex data-[type=dialog]:items-center data-[type=dialog]:justify-center",
"data-[type=floating]:absolute data-[type=tooltip]:absolute"
)}
data-slot="tour-positioner"
{...props}
/>
);
interface TourContentProps
extends React.ComponentProps<typeof ArkTour.Content> {
/**
* Show close button at the top right corner
*
* @default true
*/
showCloseButton?: boolean;
}
export const TourContent = (props: TourContentProps) => {
const { showCloseButton = true, className, children, ...rest } = props;
return (
<Portal>
<TourOverlay />
<TourPositioner>
<ArkTour.Content
className={cn(
"[--space:--spacing(4)]",
"z-[calc(50+var(--layer-index,0))]",
"relative",
"w-[min(100%,24rem)] max-w-md",
"flex flex-col gap-4",
"bg-background",
"rounded-lg border shadow-lg",
"focus:outline-none focus:ring-0",
"data-[state=closed]:animate-out data-[state=open]:animate-in",
"data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0",
"data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95",
"motion-reduce:animate-none!",
className
)}
data-slot="tour-content"
{...rest}
>
{children}
{!!showCloseButton && (
<TourClose asChild className="absolute top-4 right-4">
<Button
className="size-8 border-none opacity-70 hover:opacity-100"
size="icon-md"
variant="ghost"
>
<X />
<span className="sr-only">Close</span>
</Button>
</TourClose>
)}
</ArkTour.Content>
</TourPositioner>
<TourSpotlight />
</Portal>
);
};
export const TourBody = (props: React.ComponentProps<typeof DialogBody>) => (
<DialogBody data-slot="tour-body" {...props} />
);
export const TourSpotlight = (
props: React.ComponentProps<typeof ArkTour.Spotlight>
) => (
<ArkTour.Spotlight
className="z-50 border-2 border-primary"
data-slot="tour-spotlight"
{...props}
/>
);
export const TourHeader = (
props: React.ComponentProps<typeof DialogHeader>
) => <DialogHeader data-slot="tour-header" {...props} />;
export const TourTitle = (
props: React.ComponentProps<typeof ArkTour.Title>
) => {
const { className, ...rest } = props;
const { tour } = useTourContext();
return (
<ArkTour.Title
className={cn(
"font-semibold text-base leading-none tracking-tight",
className
)}
data-slot="tour-title"
{...rest}
>
{tour.step?.title}
</ArkTour.Title>
);
};
export const TourDescription = (
props: React.ComponentProps<typeof ArkTour.Description>
) => {
const { className, ...rest } = props;
const { tour } = useTourContext();
return (
<ArkTour.Description
className={cn("text-muted-foreground text-sm", className)}
data-slot="tour-description"
{...rest}
>
{tour.step?.description}
</ArkTour.Description>
);
};
export const TourProgressText = (
props: React.ComponentProps<typeof ArkTour.ProgressText>
) => {
const { className, ...rest } = props;
const { tour } = useTourContext();
return (
<ArkTour.ProgressText
className={cn("text-muted-foreground text-sm", className)}
data-slot="tour-progress-text"
{...rest}
>
{tour.getProgressText()}
</ArkTour.ProgressText>
);
};
export const TourClose = (
props: React.ComponentProps<typeof ArkTour.CloseTrigger>
) => <ArkTour.CloseTrigger data-slot="tour-close-trigger" {...props} />;
export const TourFooter = (
props: React.ComponentProps<typeof DialogFooter>
) => {
const { children, ...rest } = props;
return (
<ArkTour.Control {...rest} asChild>
<DialogFooter data-slot="tour-control">{children}</DialogFooter>
</ArkTour.Control>
);
};
export const TourActions = (
props: React.ComponentProps<typeof DialogFooter>
) => {
const { className, ...rest } = props;
const { tour } = useTourContext();
const actions = tour.step?.actions ?? [];
if (actions.length === 0) {
return null;
}
return (
<ArkTour.Control {...rest} asChild>
<DialogFooter
className={cn("flex flex-wrap gap-2", className)}
data-slot="tour-actions"
>
{actions.map((action) => (
<TourActionTrigger action={action} asChild key={action.label}>
<Button
size="sm"
variant={
action.action === "dismiss" || action.action === "prev"
? "outline"
: "default"
}
>
{action.action === "prev" && <ChevronLeft />}
{action.label}
{action.action === "next" && <ChevronRight />}
</Button>
</TourActionTrigger>
))}
</DialogFooter>
</ArkTour.Control>
);
};
export const TourPreviousStep = (
props: Omit<React.ComponentProps<typeof TourActionTrigger>, "action">
) => {
const { ...rest } = props;
const { tour } = useTourContext();
const prevAction = React.useMemo(
() => tour.step?.actions?.find((action) => action.action === "prev"),
[tour]
);
if (!prevAction) {
return null;
}
return (
<TourActionTrigger
data-slot="tour-previous-step"
{...rest}
action={prevAction}
asChild
>
<Button size="sm" variant="outline">
<ChevronLeft />
{prevAction.label}
</Button>
</TourActionTrigger>
);
};
export const TourNextStep = (
props: Omit<React.ComponentProps<typeof TourActionTrigger>, "action">
) => {
const { ...rest } = props;
const { tour } = useTourContext();
const action = React.useMemo(
() =>
tour.step?.actions?.find(
(a) => a.action === "next" || a.action === "dismiss"
),
[tour]
);
const actionType = React.useMemo(() => action?.action, [action]);
if (!action) {
return null;
}
return (
<TourActionTrigger
data-slot="tour-next-step"
{...rest}
action={action}
asChild
>
<Button size="sm">
{action.label}
{actionType === "next" && <ChevronRight />}
</Button>
</TourActionTrigger>
);
};
export const useTourContext = () => {
const context = React.use(TourProvider);
if (!context) {
throw new Error("useTour must be used within a TourProvider");
}
return context;
};Update the import paths to match your project setup.
Anatomy
Tour
├── TourTrigger
├── TourActionTrigger
├── TourOverlay
└── TourContent
├── TourSpotlight
├── TourHeader
│ ├── TourProgressText
│ ├── TourTitle
│ └── TourDescription
├── TourBody
├── TourFooter
│ ├── TourActions
│ ├── TourPreviousStep
│ └── TourNextStep
└── TourCloseUsage
import {
Tour,
TourTrigger,
TourContent,
TourHeader,
TourProgressText,
TourTitle,
TourDescription,
TourFooter,
TourNextStep,
TourPreviousStep,
} from "@/components/ui/tour";<Tour steps={steps}>
<TourTrigger>Open Tour</TourTrigger>
<TourContent>
<TourHeader>
<TourProgressText />
<TourTitle />
<TourDescription />
</TourHeader>
</TourContent>
<TourFooter>
<TourNextStep />
<TourPreviousStep />
</TourFooter>
</Tour>Examples
Step Types
Demonstrate all three step types in a single tour: dialog for welcome/completion, tooltip anchored to elements, and floating for fixed-position content.
Progress
Display a visual progress indicator at the bottom of the tour content showing how far along the user is.
Skip
Allow users to skip the entire tour at any step by adding a skip action.
Keyboard Navigation
Enable arrow key navigation between tour steps using the keyboardNavigation prop.
Events
Listen to tour lifecycle events like onStepChange and onStatusChange to track user progress.
Wait for Click
Use the effect function with waitForEvent to wait for user interaction before proceeding to the next step.
Wait for Input
Create form tutorials that wait for users to enter valid input before advancing.
Wait for Element
Wait for dynamically rendered elements to appear in the DOM before showing a step.
Async
Load data asynchronously and update step content before displaying it using the effect function with show() and update().
Custom spacing
Use [--space:--spacing("value")] on TourContent to adjust internal spacing.
Default spacing is --spacing(4).
You can use breakpoint utilities to change the internal spacing at different screen sizes.
md:[--space:--spacing(6)] lg:[--space:--spacing(8)]API Reference
Tour
| Prop | Type | Default |
|---|---|---|
steps | TourStepDetails[] | [] |
lazyMount | boolean | true |
unmountOnExit | boolean | true |
keyboardNavigation | boolean | - |
onStepChange | (details: { stepId: string | null }) => void | - |
onStatusChange | (details: { status: string }) => void | - |
immediate | boolean | - |
present | boolean | - |
onExitComplete | () => void | - |
TourTrigger
| Prop | Type | Default |
|---|---|---|
className | string | - |
TourContent
| Prop | Type | Default |
|---|---|---|
showCloseButton | boolean | true |
className | string | - |
asChild | boolean | - |
| Attribute | Default |
|---|---|
--space | --spacing(4) |
TourHeader
| Prop | Type | Default |
|---|---|---|
className | string | - |
TourTitle
| Prop | Type | Default |
|---|---|---|
className | string | - |
TourDescription
| Prop | Type | Default |
|---|---|---|
className | string | - |
TourProgressText
| Prop | Type | Default |
|---|---|---|
className | string | - |
TourFooter
| Prop | Type | Default |
|---|---|---|
className | string | - |
TourActions
| Prop | Type | Default |
|---|---|---|
className | string | - |
TourPreviousStep
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
TourNextStep
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
TourClose
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
useTourContext
| Return | Type |
|---|---|
tour | UseTourReturn |
handleStart | () => void |
For a complete list of props, see the Ark UI documentation.