Tooltip
A shadcn-style tooltip component built with Ark UI primitives.
I am a tooltip.
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipPopup, TooltipTrigger } from "@/components/ui/tooltip";
const TooltipBasicDemo = () => (
<Tooltip>
<TooltipTrigger>
<Button variant="outline">Hover me</Button>
</TooltipTrigger>
<TooltipPopup>I am a tooltip.</TooltipPopup>
</Tooltip>
);
export default TooltipBasicDemo;
Installation
npx shadcn@latest add @ark-cn/tooltipInstall the dependency required by this primitive:
npm install @ark-ui/reactCopy the component source into your app:
TSXcomponents/ui/tooltip.tsx
"use client";
import { Portal } from "@ark-ui/react/portal";
import {
Tooltip as TooltipPrimitive,
useTooltip,
useTooltipContext,
} from "@ark-ui/react/tooltip";
import { cn } from "@/lib/utils";
export type TooltipProps = TooltipPrimitive.RootProps;
export const Tooltip = ({
positioning = { placement: "top" },
...props
}: TooltipProps) => (
<TooltipPrimitive.Root
data-slot="tooltip"
{...props}
positioning={positioning}
/>
);
export type TooltipPopupProps = TooltipPrimitive.ContentProps & {
arrowClassName?: string;
arrowTipClassName?: string;
disablePortal?: boolean;
positionerClassName?: string;
showArrow?: boolean;
};
export const TooltipPopup = ({
arrowClassName,
arrowTipClassName,
children,
className,
disablePortal,
positionerClassName,
showArrow = true,
...contentProps
}: TooltipPopupProps) => {
const inner = (
<TooltipPrimitive.Positioner
className={cn(!disablePortal && "z-50", positionerClassName)}
data-slot="tooltip-positioner"
>
<TooltipPrimitive.Content
className={cn(
"relative z-[calc(50+var(--layer-index,0))] w-fit max-w-[min(22rem,var(--available-width,22rem))] rounded-md border border-border/80 bg-popover px-2.5 py-1.5 text-popover-foreground text-xs shadow-md outline-none ring-1 ring-border/20",
"origin-(--transform-origin) transition-opacity duration-150 data-[state=closed]:opacity-0 data-[state=open]:opacity-100",
className,
)}
data-slot="tooltip-content"
{...contentProps}
>
{showArrow ? (
<TooltipPrimitive.Arrow
className={cn(
"[--arrow-background:var(--popover)] [--arrow-size:9px] [--arrow-shadow-color:var(--border)]",
arrowClassName,
)}
data-slot="tooltip-arrow"
>
<TooltipPrimitive.ArrowTip
className={cn(
"border-border border-t border-l",
arrowTipClassName,
)}
data-slot="tooltip-arrow-tip"
/>
</TooltipPrimitive.Arrow>
) : null}
{children}
</TooltipPrimitive.Content>
</TooltipPrimitive.Positioner>
);
return disablePortal ? inner : <Portal>{inner}</Portal>;
};
export type TooltipTriggerProps = TooltipPrimitive.TriggerProps;
export const TooltipTrigger = ({
className,
...props
}: TooltipTriggerProps) => (
<TooltipPrimitive.Trigger
className={cn(className)}
data-slot="tooltip-trigger"
{...props}
/>
);
export type TooltipPositionerProps = TooltipPrimitive.PositionerProps;
export const TooltipPositioner = ({
className,
...props
}: TooltipPositionerProps) => (
<TooltipPrimitive.Positioner
className={cn(className)}
data-slot="tooltip-positioner-raw"
{...props}
/>
);
export type TooltipContentProps = TooltipPrimitive.ContentProps;
export const TooltipContent = ({
className,
...props
}: TooltipContentProps) => (
<TooltipPrimitive.Content
className={cn(className)}
data-slot="tooltip-content-raw"
{...props}
/>
);
export type TooltipArrowProps = TooltipPrimitive.ArrowProps;
export const TooltipArrow = ({ className, ...props }: TooltipArrowProps) => (
<TooltipPrimitive.Arrow
className={cn(className)}
data-slot="tooltip-arrow-raw"
{...props}
/>
);
export type TooltipArrowTipProps = TooltipPrimitive.ArrowTipProps;
export const TooltipArrowTip = ({
className,
...props
}: TooltipArrowTipProps) => (
<TooltipPrimitive.ArrowTip
className={cn(className)}
data-slot="tooltip-arrow-tip-raw"
{...props}
/>
);
export const TooltipContext = TooltipPrimitive.Context;
export const TooltipProvider = TooltipPrimitive.RootProvider;
export type TooltipRootProviderProps = TooltipPrimitive.RootProviderProps;
export const TooltipRootProvider = (props: TooltipRootProviderProps) => (
<TooltipPrimitive.RootProvider data-slot="tooltip-root-provider" {...props} />
);
export { useTooltip, useTooltipContext };
Update import aliases to match your project setup.
Usage
import * as Tooltip from "@/components/ui/tooltip"Read exported parts in src/components/ui/tooltip.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Basic
I am a tooltip.
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipPopup, TooltipTrigger } from "@/components/ui/tooltip";
const TooltipBasicDemo = () => (
<Tooltip>
<TooltipTrigger>
<Button variant="outline">Hover me</Button>
</TooltipTrigger>
<TooltipPopup>I am a tooltip.</TooltipPopup>
</Tooltip>
);
export default TooltipBasicDemo;
Arrow
Tooltip with arrow
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipPopup, TooltipTrigger } from "@/components/ui/tooltip";
const TooltipArrowDemo = () => (
<Tooltip>
<TooltipTrigger>
<Button variant="outline">Hover me</Button>
</TooltipTrigger>
<TooltipPopup showArrow>Tooltip with arrow</TooltipPopup>
</Tooltip>
);
export default TooltipArrowDemo;
Context
Tooltip open: false
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContext,
TooltipPopup,
TooltipTrigger,
} from "@/components/ui/tooltip";
const TooltipContextDemo = () => (
<Tooltip>
<TooltipTrigger>
<Button variant="outline">Hover me</Button>
</TooltipTrigger>
<TooltipPopup>
<TooltipContext>
{(api) => `Tooltip open: ${String(api.open)}`}
</TooltipContext>
</TooltipPopup>
</Tooltip>
);
export default TooltipContextDemo;
Controlled
Controlled tooltip
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipPopup, TooltipTrigger } from "@/components/ui/tooltip";
const TooltipControlledDemo = () => {
const [open, setOpen] = useState(false);
return (
<div className="flex flex-wrap items-center gap-2">
<Button
onClick={() => setOpen((prev) => !prev)}
type="button"
variant="outline"
>
Toggle
</Button>
<Tooltip open={open} onOpenChange={(details) => setOpen(details.open)}>
<TooltipTrigger>
<Button variant="outline">Hover me</Button>
</TooltipTrigger>
<TooltipPopup showArrow>Controlled tooltip</TooltipPopup>
</Tooltip>
</div>
);
};
export default TooltipControlledDemo;
Delay
No open/close delay
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipPopup, TooltipTrigger } from "@/components/ui/tooltip";
const TooltipDelayDemo = () => (
<Tooltip closeDelay={0} openDelay={0}>
<TooltipTrigger>
<Button variant="outline">Instant tooltip</Button>
</TooltipTrigger>
<TooltipPopup>No open/close delay</TooltipPopup>
</Tooltip>
);
export default TooltipDelayDemo;
Positioning
Custom offset + placement
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipPopup, TooltipTrigger } from "@/components/ui/tooltip";
const TooltipPositioningDemo = () => (
<Tooltip
positioning={{
offset: { crossAxis: 12, mainAxis: 12 },
placement: "left-start",
}}
>
<TooltipTrigger>
<Button variant="outline">Left start</Button>
</TooltipTrigger>
<TooltipPopup showArrow>Custom offset + placement</TooltipPopup>
</Tooltip>
);
export default TooltipPositioningDemo;
Different Placements
Top placement
Right placement
Bottom placement
Left placement
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipPopup, TooltipTrigger } from "@/components/ui/tooltip";
const placements = [
{ label: "Top", placement: "top" },
{ label: "Right", placement: "right" },
{ label: "Bottom", placement: "bottom" },
{ label: "Left", placement: "left" },
] as const;
const TooltipPlacementsDemo = () => (
<div className="grid grid-cols-2 gap-3">
{placements.map((item) => (
<Tooltip key={item.placement} positioning={{ placement: item.placement }}>
<TooltipTrigger>
<Button variant="outline">{item.label}</Button>
</TooltipTrigger>
<TooltipPopup showArrow>{item.label} placement</TooltipPopup>
</Tooltip>
))}
</div>
);
export default TooltipPlacementsDemo;
Root Provider
RootProvider store
import { Button } from "@/components/ui/button";
import {
TooltipPopup,
TooltipRootProvider,
TooltipTrigger,
useTooltip,
} from "@/components/ui/tooltip";
const TooltipRootProviderDemo = () => {
const tooltip = useTooltip();
return (
<div className="flex flex-col items-start gap-2">
<output className="text-muted-foreground text-xs">
Open:{" "}
<span className="font-medium text-foreground">
{String(tooltip.open)}
</span>
</output>
<TooltipRootProvider value={tooltip}>
<TooltipTrigger>
<Button variant="outline">Hover me</Button>
</TooltipTrigger>
<TooltipPopup>RootProvider store</TooltipPopup>
</TooltipRootProvider>
</div>
);
};
export default TooltipRootProviderDemo;
Within Fixed Container
strategy=fixed
Hover the fixed trigger in the bottom-right viewport corner.
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipPopup, TooltipTrigger } from "@/components/ui/tooltip";
const TooltipWithinFixedDemo = () => (
<div className="w-full rounded-lg border border-border/70 bg-muted/15 p-3">
<div className="fixed right-4 bottom-4 z-40 rounded-lg bg-primary/15 p-4 shadow-md">
<Tooltip positioning={{ strategy: "fixed" }}>
<TooltipTrigger>
<Button size="sm" variant="outline">
Fixed trigger
</Button>
</TooltipTrigger>
<TooltipPopup showArrow>strategy=fixed</TooltipPopup>
</Tooltip>
</div>
<p className="text-muted-foreground text-xs">
Hover the fixed trigger in the bottom-right viewport corner.
</p>
</div>
);
export default TooltipWithinFixedDemo;
API reference
This component mirrors the upstream Ark UI primitive. All props and DOM behavior are defined by Ark unless you see an ark-cn-only row below.
TooltipPopup
| Prop | Type | Description |
|---|---|---|
| arrowClassName? | string | Class on the arrow element. |
| arrowTipClassName? | string | Class on the arrow tip. |
| disablePortal? | boolean | Renders content in-place instead of portaled. |
| positionerClassName? | string | Extra class on the positioner wrapper. |
| showArrow? | boolean | Toggles the arrow (default true). |
See the ARK UI documentation for the full API.
Accessibility
See the Ark UI documentation for clarification.