Composite
Tooltip
Tooltip exposes the low-level built-in primitives plus a SimpleTooltip convenience wrapper for the common single-trigger case.
Installation
npx shadcn add https://rck.vibeboxph.com//r/tooltip.jsonLive preview & controls
Controls
<SimpleTooltip content="Helpful supplementary text">
<Button variant="outline">Hover for details</Button>
</SimpleTooltip>Anatomy
TooltipProviderShares delay timings across a tree of tooltips.TooltipThe root, holds open state.TooltipTriggerThe element that opens the tooltip on hover/focus.TooltipContentThe floating panel with an arrow.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| content (SimpleTooltip)* | React.ReactNode | — | Tooltip body. |
| placement | "top" | "right" | "bottom" | "left" | top | Preferred placement, flips automatically if there's no room. |
| delayDuration | number | 200 | Hover delay in ms before opening. |
| open / onOpenChange | boolean / (open: boolean) => void | — | Controlled open state (for click-triggered tooltips). |
Accessibility
- Opens on both hover and keyboard focus, and closes on Escape — never hover-only.
- Uses `role="tooltip"` and is associated to the trigger through `aria-describedby`.
- Content should stay to short, supplementary text — it's not a substitute for visible labels.
Source
registry/rck/ui/tooltip.tsx
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@/registry/rck/primitives/tooltip";
import { cn } from "@/lib/utils";
export const TooltipProvider = TooltipPrimitive.Provider;
export const Tooltip = TooltipPrimitive.Root;
export const TooltipTrigger = TooltipPrimitive.Trigger;
export const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 6, children, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"group bg-foreground text-background z-50 max-w-64 rounded-md px-2.5 py-1.5 text-xs shadow-md",
"data-[state=delayed-open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
"data-[state=delayed-open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=delayed-open]:zoom-in-95",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow
aria-hidden="true"
className={cn(
"fill-foreground absolute",
"group-data-[side=top]:-bottom-1 group-data-[side=top]:left-1/2 group-data-[side=top]:-translate-x-1/2",
"group-data-[side=bottom]:-top-1 group-data-[side=bottom]:left-1/2 group-data-[side=bottom]:-translate-x-1/2 group-data-[side=bottom]:rotate-180",
"group-data-[side=right]:top-1/2 group-data-[side=right]:-left-1 group-data-[side=right]:-translate-y-1/2 group-data-[side=right]:rotate-90",
"group-data-[side=left]:top-1/2 group-data-[side=left]:-right-1 group-data-[side=left]:-translate-y-1/2 group-data-[side=left]:-rotate-90",
)}
/>
</TooltipPrimitive.Content>
));
TooltipContent.displayName = "TooltipContent";
/**
* Convenience wrapper for the common case: a single trigger with plain-text
* or simple-node content. Use the primitives above directly for full control
* (custom delays per tooltip, rich interactive content, etc).
*/
export interface SimpleTooltipProps extends Omit<
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>,
"content"
> {
content: React.ReactNode;
children: React.ReactElement;
delayDuration?: number;
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
export function SimpleTooltip({
content,
children,
delayDuration = 200,
defaultOpen,
open,
onOpenChange,
...contentProps
}: SimpleTooltipProps) {
return (
<TooltipProvider delayDuration={delayDuration}>
<Tooltip defaultOpen={defaultOpen} open={open} onOpenChange={onOpenChange}>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent {...contentProps}>{content}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
Related examples
PlacementTop, right, bottom and left.
Trigger modesDefault hover/focus vs. explicit click control.
Content lengthShort vs. wrapped long content.