COMPONENT / hud-tooltip
HUD Tooltip_
Hover and focus tooltip in two looks: a bordered data readout and a target-lock card with bracket corners and a leader line. Built on Base UI. Requires @cybr/tokens.
Install
1. Map the
@cybrnamespace, once per project. Add this inside yourcomponents.json, or into its existing"registries"object. Nocomponents.jsonyet? Runnpx shadcn@latest initfirst to create one."registries": { "@cybr": "https://cybr-ui.com/r/{name}.json" }2. Add components:
$ npx shadcn@latest add @cybr/hud-tooltip
If shadcn reports Unknown registry "@cybr", step 1 is missing from that project's components.json.
Preview
registry/ui/hud-tooltip.tsx
"use client";
import { Tooltip } from "@base-ui/react/tooltip";
import { useId, useState, type ReactElement, type ReactNode } from "react";
import { cn } from "@/lib/utils";
const popupVariants = {
readout:
"w-50 bg-ground-void border-(length:--border-hair) border-signal px-3 py-2.5 font-mono text-caption [line-height:1.6] tracking-[0.06em] text-ink",
lock: "relative w-55.5 bg-ground-void border-(length:--border-hair) border-line px-4 py-3.5 font-mono text-ink",
};
// Leader stub and node on the popup edge that faces the trigger, following
// Base UI's data-side so they stay correct when the popup flips.
const leaderStub =
"pointer-events-none absolute bg-signal " +
"group-data-[side=bottom]/tip:bottom-full group-data-[side=bottom]/tip:left-5 group-data-[side=bottom]/tip:h-3 group-data-[side=bottom]/tip:w-0.5 " +
"group-data-[side=top]/tip:top-full group-data-[side=top]/tip:left-5 group-data-[side=top]/tip:h-3 group-data-[side=top]/tip:w-0.5 " +
"group-data-[side=right]/tip:right-full group-data-[side=right]/tip:top-5 group-data-[side=right]/tip:h-0.5 group-data-[side=right]/tip:w-3 " +
"group-data-[side=left]/tip:left-full group-data-[side=left]/tip:top-5 group-data-[side=left]/tip:h-0.5 group-data-[side=left]/tip:w-3";
const leaderNode =
"pointer-events-none absolute size-2.5 bg-signal " +
"group-data-[side=bottom]/tip:-top-1.25 group-data-[side=bottom]/tip:left-4 " +
"group-data-[side=top]/tip:-bottom-1.25 group-data-[side=top]/tip:left-4 " +
"group-data-[side=right]/tip:-left-1.25 group-data-[side=right]/tip:top-4 " +
"group-data-[side=left]/tip:-right-1.25 group-data-[side=left]/tip:top-4";
// HUD tooltip built on Base UI. Opens on hover (after 150ms) and immediately on
// keyboard focus, closes on Escape, and stays open while the pointer moves into
// it. `readout` is a small bordered data box; `lock` is the target-lock card
// with bracket corners and a leader line. Tooltips are supplementary: they are
// not shown on touch, so never put essential information only in one.
export function HudTooltip({
content,
children,
variant = "readout",
eyebrow,
title,
side = "bottom",
align = "start",
open,
defaultOpen,
onOpenChange,
className,
...props
}: Omit<Tooltip.Popup.Props, "children" | "className" | "title"> & {
content: ReactNode;
children: ReactElement;
variant?: "readout" | "lock";
eyebrow?: ReactNode;
title?: ReactNode;
side?: "top" | "right" | "bottom" | "left";
align?: "start" | "center" | "end";
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: Tooltip.Root.Props["onOpenChange"];
className?: string;
}) {
const id = useId();
// Mirror the open state so aria-describedby only points at a mounted popup.
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen ?? false);
const isOpen = open ?? uncontrolledOpen;
return (
<Tooltip.Provider>
<Tooltip.Root
open={open}
defaultOpen={defaultOpen}
onOpenChange={(next, details) => {
setUncontrolledOpen(next);
onOpenChange?.(next, details);
}}
>
<Tooltip.Trigger delay={150} render={children} aria-describedby={isOpen ? id : undefined} />
<Tooltip.Portal>
<Tooltip.Positioner side={side} align={align} sideOffset={12} className="z-50">
<Tooltip.Popup
id={id}
role="tooltip"
className={cn("group/tip", popupVariants[variant], className)}
{...props}
>
{variant === "lock" ? (
<>
<span
aria-hidden="true"
className="pointer-events-none absolute -top-1 -left-1 size-3 border-t-(length:--border-heavy) border-l-(length:--border-heavy) border-signal"
/>
<span
aria-hidden="true"
className="pointer-events-none absolute -right-1 -bottom-1 size-3 border-r-(length:--border-heavy) border-b-(length:--border-heavy) border-signal"
/>
<span aria-hidden="true" className={leaderStub} />
<span aria-hidden="true" className={leaderNode} />
{eyebrow ? (
<span className="mb-1.5 block text-micro tracking-[0.16em] text-label">{eyebrow}</span>
) : null}
{title ? (
<span className="mb-2 block font-display text-xl [line-height:1] uppercase">{title}</span>
) : null}
<span className="block text-caption [line-height:1.6] text-label">{content}</span>
</>
) : (
content
)}
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}
Repo source. The shadcn CLI rewrites import aliases to match your project.
Hover over or tab to a node to open its tooltip.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| content | ReactNode | — | Tooltip text. In the lock variant, this is the body under the title. |
| children | ReactElement | — | One focusable element, such as a <button>, used as the trigger. Give it its own accessible name. |
| variant | "readout" | "lock" | "readout" | Readout is a small bordered data box. Lock is the target-lock card with bracket corners and a leader line. |
| eyebrow | ReactNode | — | Small label above the title. Lock variant only. |
| title | ReactNode | — | Display title. Lock variant only. |
| side | "top" | "right" | "bottom" | "left" | "bottom" | Preferred side. It flips when there is no room. |
| align | "start" | "center" | "end" | "start" | Alignment along that side. |
| open / defaultOpen / onOpenChange | boolean / boolean / function | — | Control the open state, for example to pin a tooltip open. |
| ...props | Tooltip.Popup props | — | Forwarded to Base UI's Tooltip.Popup; className is merged last with cn(). |
Usage
Installs @base-ui/react. Needs the stylesheet from @cybr/tokens. After the install, import it in app/globals.css:
@import "tailwindcss";
@import "../styles/cybr-tokens.css";
import { HudTooltip } from "@/components/ui/hud-tooltip";
export function Node() {
return (
<HudTooltip variant="lock" eyebrow="TARGET LOCKED" title="NODE 12" content="Relay online.">
<button type="button">NODE 12</button>
</HudTooltip>
);
}
States
- Closed: nothing is rendered.
- Open: appears instantly, with no fade. Hover opens it after 150ms, and keyboard focus opens it straight away.
- Hovering the popup: it stays open while the pointer moves from the trigger into it.
- Flipped: when there is no room on the chosen side, it moves to the opposite side. The lock leader line moves with it to the edge facing the trigger.
- Pinned: pass
openordefaultOpen. The artboard's "pinned" tooltip is this state; there is no separate component.
Accessibility
- Built on Base UI's Tooltip. It opens on hover and on keyboard focus, and Escape closes it.
- While open, the popup has
role="tooltip"and the trigger points at it witharia-describedby. The trigger still needs its own accessible name, such as its text. - Tooltips are supplementary. Base UI does not show them on touch devices, so never put information that people need only in a tooltip.
- Bracket corners, the leader line and its node are decorative and hidden from assistive tech.
- Text is
--ink(readout) or--inkand--label(lock) on--ground-void, which is darker than--bg, so contrast is at least the 14.6:1 and 7.43:1 measured on--bg. If you override colours withclassName, keep text contrast at 4.5:1 or above.