Back

Component Preview Tooltip

A hover tooltip that shows a live preview of any component.

Category
Micro InteractionReact
CSS
Tailwind

Manual

Create a file and paste the following code into it.

component-preview-tooltip.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
"use client";

import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { type FC, type ReactNode, useEffect, useState } from "react";
import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/cn";

interface ComponentPreviewTooltipProps {
	componentName: string;
	children: ReactNode;
	width?: number;
	height?: number;
	scale?: number;
	side?: "top" | "right" | "bottom" | "left";
	className?: string;
}

export const ComponentPreviewTooltip: FC<ComponentPreviewTooltipProps> = ({
	componentName,
	children,
	width = 300,
	height = 200,
	scale = 0.8,
	side = "top",
	className,
}) => {
	const [PreviewComponent, setPreviewComponent] = useState<FC | null>(null);
	const [isLoading, setIsLoading] = useState(false);

	useEffect(() => {
		let mounted = true;

		async function loadComponent() {
			setIsLoading(true);
			try {
				const module = await import(
					`@/components/previews/${componentName}/default`
				);
				if (mounted) {
					setPreviewComponent(() => module.default);
				}
			} catch (error) {
				console.error(`Failed to load preview for ${componentName}`, error);
			} finally {
				if (mounted) {
					setIsLoading(false);
				}
			}
		}

		loadComponent();

		return () => {
			mounted = false;
		};
	}, [componentName]);

	return (
		<Tooltip delayDuration={0}>
			<TooltipTrigger asChild>
				<span className={cn("inline-block cursor-help", className)}>
					{children}
				</span>
			</TooltipTrigger>

			<TooltipPrimitive.Portal>
				<TooltipPrimitive.Content
					side={side}
					sideOffset={5}
					className={cn(
						"z-50 overflow-hidden rounded-3xl border shadow-xl",
						"bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800",
						"text-zinc-950 dark:text-zinc-50",
						"animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
						"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
					)}
					style={{
						width,
						height,
					}}
				>
					<div className="flex h-full w-full flex-col p-3">
						<div className="flex-1 overflow-hidden relative bg-zinc-50 dark:bg-zinc-950/50 rounded-2xl">
							{isLoading ? (
								<div className="absolute inset-0 flex items-center justify-center">
									<div className="h-8 w-8 animate-spin rounded-full border-2 border-zinc-300 border-t-blue-500" />
								</div>
							) : PreviewComponent ? (
								<div
									className="origin-top-left absolute top-0 left-0"
									style={{
										width: `${100 / scale}%`,
										height: `${100 / scale}%`,
										transform: `scale(${scale})`,
									}}
								>
									<div className="flex w-full h-full items-center justify-center p-4">
										<PreviewComponent />
									</div>
								</div>
							) : (
								<div className="absolute inset-0 flex items-center justify-center text-sm text-zinc-500">
									Preview not found
								</div>
							)}
						</div>
					</div>
					<TooltipPrimitive.Arrow
						className="fill-white dark:fill-zinc-900"
						width={11}
						height={5}
					/>
				</TooltipPrimitive.Content>
			</TooltipPrimitive.Portal>
		</Tooltip>
	);
};

Update the import paths to match your project setup.

Similar screens