All components
Anchored Product Tour
tooltipsOrigin UI component.
responsive · 480px
Install
Same command in any shadcn project — React (Vite/CRA), Next.js, Remix, Astro, and more:
$
npx shadcn@latest add https://your-domain/r/comp-389.jsonUsage
import Cmp from "@/registry/origin-ui/comp-389";
export default function Demo() {
return <Cmp />;
}Component source
"use client";
import {
ClubIcon,
DiamondIcon,
HeartIcon,
type LucideIcon,
SpadeIcon,
} from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverAnchor,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
interface TourStep {
icon: LucideIcon;
title: string;
description: string;
}
const tourSteps: TourStep[] = [
{
description:
"This is your new workspace. Here you'll find all your projects, recent activities, settings, and more.",
icon: HeartIcon,
title: "Heart",
},
{
description:
"Use the toolbar above to create new projects, invite team members, or access settings.",
icon: DiamondIcon,
title: "Diamond",
},
{
description:
"Click the support icon in the top right corner to access our help center and documentation.",
icon: ClubIcon,
title: "Club",
},
{
description:
"Press ⌘K to open the command palette. Use arrow keys to navigate and Enter to select an action.",
icon: SpadeIcon,
title: "Spade",
},
];
interface CardProps {
number: number;
isActive: boolean;
}
function Card({ number, isActive }: CardProps) {
const content = (
<div className="flex size-10 items-center justify-center rounded-md bg-secondary font-medium text-muted-foreground text-sm">
{number + 1}
</div>
);
return isActive ? <PopoverAnchor>{content}</PopoverAnchor> : content;
}
export default function Component() {
const [currentTip, setCurrentTip] = useState(0);
const handleNavigation = () => {
if (currentTip === tourSteps.length - 1) {
setCurrentTip(0);
} else {
setCurrentTip(currentTip + 1);
}
};
return (
<div className="flex flex-col gap-4">
<Popover
onOpenChange={(open) => {
if (open) setCurrentTip(0);
}}
>
<div className="grid grid-cols-2 place-items-center gap-4">
{tourSteps.map((step, index) => (
<Card
isActive={currentTip === index}
key={step.title}
number={index}
/>
))}
</div>
<PopoverTrigger asChild>
<Button variant="outline">Start tour</Button>
</PopoverTrigger>
<PopoverContent
className="max-w-[280px] py-3 shadow-none"
showArrow={true}
side={currentTip % 2 === 0 ? "left" : "right"}
>
<div className="space-y-3">
<div className="space-y-1">
<p className="font-medium text-[13px]">
{tourSteps[currentTip].title}
</p>
<p className="text-muted-foreground text-xs">
{tourSteps[currentTip].description}
</p>
</div>
<div className="flex items-center justify-between gap-2">
<span className="text-muted-foreground text-xs">
{currentTip + 1}/{tourSteps.length}
</span>
<button
className="font-medium text-xs hover:underline"
onClick={handleNavigation}
type="button"
>
{currentTip === tourSteps.length - 1 ? "Start over" : "Next"}
</button>
</div>
</div>
</PopoverContent>
</Popover>
</div>
);
}Source: Origin UI