shadcn.io is not affiliated with official shadcn/ui
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.jsonThis component depends on Button and Dialog. Install them first if you haven't already.
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
TourContent
TourActionTrigger
TourClose
TourProgressText
TourTitle
TourDescription
TourPositioner
TourArrow
└── TourArrowTip
TourOverlay
TourSpotlightUsage
import React from "react";
import { Button } from "@/components/ui/button";
import {
Tour,
TourContent,
TourDescription,
TourFooter,
TourHeader,
TourNextStep,
TourPreviousStep,
TourProgressText,
type TourStepType,
TourTitle,
TourTrigger,
} from "@/components/ui/tour";const TourDemo = () => {
const installRef = React.useRef<HTMLButtonElement>(null);
const usageRef = React.useRef<HTMLDivElement>(null);
const steps = React.useMemo<TourStepType[]>(
() => [
{
actions: [{ action: "next", label: "Start tour" }],
description:
"This walkthrough stays inside the preview. Next steps highlight pieces of this mini app.",
id: "welcome",
title: "Welcome",
type: "dialog",
},
{
actions: [
{ action: "prev", label: "Back" },
{ action: "next", label: "Next" },
],
description: "Use the CLI or a manual install from this control.",
id: "install",
placement: "bottom",
target: () => installRef.current,
title: "Install",
type: "tooltip",
},
{
actions: [
{ action: "prev", label: "Back" },
{ action: "next", label: "Next" },
],
description:
"Usage notes live next to install so you can copy a snippet.",
id: "usage",
placement: "top",
target: () => usageRef.current,
title: "Usage",
type: "tooltip",
},
{
actions: [{ action: "dismiss", label: "Done" }],
description:
"The overlay, spotlight, and popover are clipped to this frame.",
id: "done",
title: "That’s it",
type: "dialog",
},
],
[]
);
return (
<Tour steps={steps}>
<div className="flex w-full max-w-lg flex-col overflow-hidden rounded-xl border bg-background shadow-sm">
<header className="flex items-center justify-between gap-3 border-b px-3 py-2.5">
<span className="font-semibold text-sm">Acme UI</span>
<div className="flex items-center gap-1">
<Button ref={installRef} size="sm" type="button" variant="ghost">
Install
</Button>
<TourTrigger asChild>
<Button size="sm">Start tour</Button>
</TourTrigger>
</div>
</header>
<div className="grid gap-3 p-4 sm:grid-cols-2">
<div className="rounded-lg border bg-muted/40 p-3">
<p className="font-medium text-sm">Install</p>
<p className="mt-1 text-muted-foreground text-xs">
Add the component with the CLI, then import it.
</p>
</div>
<div className="rounded-lg border bg-muted/40 p-3" ref={usageRef}>
<p className="font-medium text-sm">Usage</p>
<p className="mt-1 text-muted-foreground text-xs">
Compose steps, then call start() from a trigger.
</p>
</div>
</div>
</div>
<TourContent>
<TourHeader>
<TourProgressText />
<TourTitle />
<TourDescription />
</TourHeader>
<TourFooter>
<TourPreviousStep />
<TourNextStep />
</TourFooter>
</TourContent>
</Tour>
);
};Accessibility
| Key | Description |
|---|---|
Enter | Activate the focused control. |
Space | Activate the focused control. |
Demos
Async
Custom Spacing
Events
Keyboard Navigation
Progress
Skip
Step Types
Wait For Click
Wait For Element
Wait For Input
API Reference
Tour
button
PropType
AttributeType
TourContent
div
PropType
AttributeType
TourActionTrigger
button
PropType
AttributeType
TourClose
button
PropType
AttributeType
TourProgressText
div
PropType
AttributeType
TourTitle
h2
PropType
AttributeType
TourDescription
div
PropType
AttributeType
TourPositioner
div
PropType
AttributeType
CSS variableType
TourOverlay
div
PropType
AttributeType
CSS variableType
TourSpotlight
div
PropType
AttributeType
CSS variableType
TourTrigger
button
PropType
AttributeType
TourBody
PropType
AttributeType
TourHeader
PropType
AttributeType
TourFooter
div
PropType
AttributeType
TourActions
div
PropType
AttributeType
TourPreviousStep
PropType
AttributeType
TourNextStep
PropType
AttributeType