Tooltip

A small floating label that appears above an anchor on hover

A state atom whose appearance (hidden, visible) is a pure function of the state prop. Pass a state directly or drive it from the Remotion timeline via useCurrentState. The component reads no frame — only the state value you give it. The tooltip fades in at 0.96 scale, slides 4 px toward the anchor, and rests fully visible; hidden reverses the motion. Placement is the caller's responsibility — the tooltip renders inline and the caller wraps it in an absolutely-positioned anchor div.

Installation

$ pnpm dlx shadcn@latest add @remocn/tooltip

Installing tooltip automatically installs the shared remocn-ui core (lib/remocn-ui/) via registryDependencies (["remocn-ui"]). You do not need to install it separately. Both tooltip (the renderer) and use-tooltip-transition (the hook) are copied into your project.

States

TooltipState is:

type TooltipState =
  | "hidden"   // opacity 0, scale 0.96, translate 4px away from anchor
  | "visible"  // opacity 1, scale 1, translate 0

hidden is the default. The translate magnitude (4 px when hidden, 0 when visible) is resolved by the component into an axis offset based on side — a "top" tooltip slides upward when hidden, a "bottom" tooltip slides downward, and so on.

Snap usage

Pass state directly — the component snaps instantly to that visual. Useful for static previews or when you drive state from your own logic:

import { Tooltip } from "@/components/remocn/tooltip";

export const Scene = () => (
  <div style={{ position: "relative", width: "100%", height: "100%" }}>
    <div style={{ position: "absolute", left: "50%", top: "40%", transform: "translate(-50%, -100%)" }}>
      <Tooltip state="visible" label="Keyboard shortcut ⌘K" side="top" />
    </div>
  </div>
);

To drive state from the timeline, use useCurrentState:

import { useCurrentState } from "@/lib/remocn-ui";
import { Tooltip } from "@/components/remocn/tooltip";

export const Scene = () => {
  const state = useCurrentState(
    [
      { at: 30, state: "visible" },
      { at: 90, state: "hidden"  },
    ],
    "hidden",
  );

  return (
    <div style={{ position: "relative", width: "100%", height: "100%" }}>
      <div style={{ position: "absolute", left: "50%", top: "40%", transform: "translate(-50%, -100%)" }}>
        <Tooltip state={state} label="Keyboard shortcut ⌘K" side="top" />
      </div>
    </div>
  );
};

Each at is a Sequence-local authored frame. State persists between steps: visible at frame 30 keeps the tooltip on-screen until hidden fires at frame 90. See Concepts for the full useCurrentState API.

Smooth transitions

State changes via state snap with no cross-fade. For an animated show/hide, use useTooltipTransition from the copied use-tooltip-transition.ts file. It reads the frame, interpolates between state presets, and returns a resolved TooltipStyle — pass it to the style prop:

import { Tooltip } from "@/components/remocn/tooltip";
import { useTooltipTransition } from "@/components/remocn/use-tooltip-transition";

export const Scene = () => {
  const style = useTooltipTransition([
    { at: 30, state: "visible", duration: 8 },
    { at: 90, state: "hidden",  duration: 8 },
  ]);

  return (
    <div style={{ position: "relative", width: "100%", height: "100%" }}>
      <div style={{ position: "absolute", left: "50%", top: "40%", transform: "translate(-50%, -100%)" }}>
        <Tooltip style={style} label="Keyboard shortcut ⌘K" side="top" />
      </div>
    </div>
  );
};

The duration field on each step overrides the file's DEFAULT_DURATION (= 8) for that specific transition. To globally tune timing and easing, edit use-tooltip-transition.ts directly in your project — that file is yours (shadcn "own your code" philosophy).

style takes precedence over state when both are provided.

Sides

The side prop controls two things: which direction the bubble slides in from when it appears, and which edge of the bubble the arrow points from. It is a static prop — it does not animate.

type TooltipSide =
  | "top"     // bubble above anchor, arrow on bottom edge, enters sliding up 4px
  | "bottom"  // bubble below anchor, arrow on top edge, enters sliding down 4px
  | "left"    // bubble left of anchor, arrow on right edge, enters sliding left 4px
  | "right"   // bubble right of anchor, arrow on left edge, enters sliding right 4px

Placement is the caller's responsibility. The tooltip renders inline — wrap it in an absolutely-positioned div that positions the bubble relative to its anchor. For a "top" tooltip:

{/* Anchor element */}
<div style={{ position: "relative", display: "inline-block" }}>
  <Button label="Hover me" style={buttonStyle} />

  {/* Tooltip positioned above the anchor, centered horizontally */}
  <div
    style={{
      position: "absolute",
      bottom: "calc(100% + 12px)",
      left: "50%",
      transform: "translateX(-50%)",
    }}
  >
    <Tooltip style={tooltipStyle} label="Keyboard shortcut ⌘K" side="top" />
  </div>
</div>

For "bottom", use top: "calc(100% + 12px)" instead. For "left" / "right", use right: "calc(100% + 12px)" / left: "calc(100% + 12px)" and adjust vertical alignment to top: "50%" with translateY(-50%).

Props

PropTypeDefaultDescription
label
stringThe tooltip text. Rendered as a single line (whitespace-nowrap).
side
"top" | "bottom" | "left" | "right""top"Which side of the anchor the tooltip sits on. Sets the enter-slide direction and which edge the arrow points from. Static — not animated.
state
"hidden" | "visible""hidden"Current visual state (snap path). State changes snap — no automatic cross-fade.
style
TooltipStyleResolved animated visual (smooth path). Pass an interpolated TooltipStyle from useTooltipTransition. Takes precedence over state when provided.
theme
Partial<RemocnTheme>Per-component theme override. Merges with the active RemocnUIProvider theme and the mode defaults. Must be concrete oklch/hex/rgb values — not CSS custom properties.
mode
"light" | "dark""light"Sets the base palette. Overrides the mode on RemocnUIProvider when both are present.
className
stringOptional className applied to the outer wrapper element.