UIDex

Tooltip

التلميح

role="tooltip" + aria-describedby

also calledhover hint، info bubble، help tip، title tip، hover label، تولتيب، تلميح الأدوات، فقاعة التلميح، تلميح عائم، شرح الأيقونة

A tooltip is a short piece of text that appears next to an element when the pointer rests on it or the keyboard focuses it. The text names the element or explains what it does. It is a description, not content: plain text with no button, link, or field, because role="tooltip" defines no navigation model inside itself, and the first interactive thing you put in it turns it into something else. Tell it apart from three look-alikes. A hover card is a rich, interactive layer with images and links, built for the pointer to enter and stay. A toggletip opens on click rather than hover, and its text is announced through an aria-live region because it answers a question the user asked. The native HTML title attribute is a tip the browser draws, with a delay you cannot set, a look you cannot style, and no keyboard or touch exposure at all. Two rules follow from that. A tooltip never holds interactive content, and it is never the only place a piece of information exists, because half your users are on a touch screen where "hover" does not exist.

If you called it…

"the little box that shows up when I hover an icon""the bubble with the tiny arrow under it""the hover text that explains what a button does""the hint that pops up after about a second""the thing that shows the full name when it is cut off"

Live specimen

Interact with the demo. Every part is real and numbered.

مرّر المؤشّر فوق أيقونة الرابط، أو اضغط Tab للوصول إليها.

نسخ الرابط إلى الحافظةCtrl + Shift + C
aria-describedby="tip-share"تأخير الفتح450ms
RTLالاختصار اللاتيني معزول باتجاهه، و Esc يُغلق التلميح

Anatomy: every part, named

Hover a row to locate it in the demo above.

Build prompt

Build a tooltip on a natively focusable trigger: a <button> or a link, never a <div> with tabindex. Render the box with role="tooltip" and a stable id, wire it with aria-describedby added on open and removed on close, and do not also set aria-label to the same string or it gets spoken twice. Content is text only: no button, link, or field inside, and the tooltip must never be the only place the information lives, because touch devices have no hover at all. Surface the same fact as visible text or in a help panel. Open after a 450 ms delay on hover, immediately on focus, and close on blur, on pointerleave, and on Escape via a document listener torn down with the close. Fold the gap between trigger and box into the hover area with transparent padding or safePolygon, so it does not close while the pointer travels into it. Portal it so it escapes overflow: hidden, and position it with flip, shift, and arrow. Write every offset as a logical property: inset-inline-start, margin-inline, padding-inline, and alignment through the -start/-end suffix rather than left/right. The single exception is the arrow offset from arrow(), which is applied with left because it is already computed in physical pixels. Drive transform-origin from a variable flipped under [dir="rtl"], since it accepts no logical keywords, and wrap any Latin token inside Arabic text in <bdi>. Honour prefers-reduced-motion by dropping the animation while keeping the delay.

How it behaves right-to-left

This section is ours alone.

Side, alignment, and edge collision

mirrors

The main side values in Floating UI and Radix (top, right, bottom, left) are physical and know nothing about page direction: a tooltip set to right stays on the right of its trigger even on an Arabic page, so derive the side yourself from a logical intent. The -start / -end alignment suffix is logical; computePosition reads direction off the computed styles and flips it, so bottom-start aligns to the right edge in RTL. The flip middleware handles flipping at the viewport edge, and direction has nothing to do with it. Any manual anchoring is written with inset-inline-start, never left.

The arrow offset

never mirrors

The value returned in middlewareData.arrow.x is physical pixels measured from the left edge of the tooltip box, already computed with page direction taken into account, so apply it literally as left: Xpx. Write it as inset-inline-start and it flips a second time, so the arrow detaches from the trigger in RTL. The face the arrow sticks out of is likewise derived from the physical side in placement (the opposite of top is bottom, the opposite of left is right), so it stays physical too, and writing it logically unhooks it from the side the engine actually resolved. This is one of the rare places where the physical property is the correct one. The general rule, inset-inline-start over left, still governs everything else in the tooltip.

Direction of the tooltip text

never mirrors

Text direction inside a tooltip follows the language of the text, not the language of the page: an Arabic tooltip explaining a function name like getUserById or a path like /var/log/app.log must isolate the Latin run with <bdi>, or dir="ltr" plus unicode-bidi: isolate, otherwise the leading slash jumps to the other end. The bigger trap is that tooltips are usually portalled to <body>, so they inherit dir from the document root rather than from the trigger. If your trigger sits inside a dir="ltr" code block, the box never finds out. Set dir explicitly on the tooltip content, or drive it from the DirectionProvider in Radix.

The enter-animation origin

mirrors

A scale-in has to grow out of the side the trigger is on, otherwise the tooltip looks like it sprouted from nowhere. The trap is that transform-origin accepts no logical keywords at all: inline-start is not a valid value, and the allowed keywords and percentages are physical, resolved from the top-left edge whatever the direction. Drive it from a variable: --tip-origin: right, then [dir="rtl"] & { --tip-origin: left }, and apply transform-origin: var(--tip-origin) center. Any translateX in the animation needs the same treatment.

The native title attribute

never mirrors

A title-based tip is painted by the browser outside the DOM tree: CSS never reaches it, and you cannot wrap part of it in <bdi>, because an attribute value is raw text that accepts no markup. So a mixed line like "افتح config.json من مجلّد build" is ordered by the bidi algorithm alone, and the Latin name, the dot, or the slash can land at the wrong end. The only lever left inside raw text is the control characters themselves: U+2066 and U+2069 to isolate, or U+200E. The better fix is to replace title with your own role="tooltip" element, where dir and <bdi> work and keyboard and touch users can see it.

In code

Each row is one framework's word for the same thing. Take the row your project speaks.

ARIArole="tooltip" + aria-describedbyThe role labels the box; describedby is what makes a screen reader speak it after the element name.
HTMLtitle="…"The native tip cannot be styled and stays invisible to keyboard and touch. It is a historical reference, not a design option.
Radix UI<Tooltip.Provider delayDuration={450}>Delay is configured on the provider, not per tooltip; skipDelayDuration drops it between neighbours in the same group.
Floating UIuseHover(ctx, { handleClose: safePolygon() })Keeps the tooltip open while the pointer is heading toward it, instead of relying on a blind close timeout.
shadcn/ui<TooltipTrigger asChild>asChild hands the behaviour to your real button instead of wrapping it in an extra, unfocusable element.
Material UI<Tooltip title enterDelay leaveDelay>Material calls the text title, yet it is a full React node, and the enter and leave delays are deliberately separate.
CSSanchor-name / position-areaNative anchor positioning; logical position-area values such as block-start follow the writing direction on their own.

See also

Updated · 2026-08-19