Tooltip + TooltipController + TooltipManipulator together drive a smart hover/focus tooltip system: smooth size/position tweens, off-screen prevention, multi-column comparisons, nested tooltips, and pooling.

Tooltip

Features

  • Smooth size/position updates with configurable fade settings
  • Auto-positioning that prevents tooltips from going off-screen
  • Multi-column layouts — perfect for inventory-vs-equipped comparisons
  • Nested tooltips with customizable locking behavior
  • Object pooling for performance with many tooltips
  • Disabled state to programmatically gate tooltip opening

Setup

  1. Place a single TooltipController MonoBehaviour in the scene; it owns the on-screen Tooltip element.
  2. For each element that should show a tooltip, add a TooltipManipulator with one or more TooltipContainerSetups.
csharp
[SerializeField] private TooltipController _tooltipController; // `root` is the visual tree delivered by your PanelRenderer's reload // callback — see the Example below. VisualElement target = root.Q<VisualElement>("TooltipOnHover"); // The tooltip's content slots are plain VisualElements you create: VisualElement image = null; // optional icon slot Label title = new Label("My Tooltip Title"); Label body = new Label("My Tooltip Description"); var setup = new TooltipContainerSetup( image, title, body, bottom: null, colorBackground: new UIColor("slate", UIColorValue.V_800), colorBorder: new UIColor("slate", UIColorValue.V_200) ); var manipulator = new TooltipManipulator(gameObject, _tooltipController); manipulator.SetSetup(setup); target.AddManipulator(manipulator);

⚠️ One TooltipManipulator instance per target element. A Manipulator attaches to one element at a time, so adding the same instance to a second element silently detaches it from the first. (The TooltipContainerSetup can be shared across manipulators.)

Inspector (UxmlAttributes)

The Tooltip element exposes fade settings:

Tooltip Inspector

AttributeDescription
FadeDurationDuration of the built-in fade-in/out animation.
FadeEasingModeEasing curve for the built-in fade.
show-transitionOptional Transition Sequence asset — replaces the built-in fade for showing tooltip items.
hide-transitionOptional sequence asset replacing the built-in fade-out; the item is hidden and pooled when the sequence completes.
debug-logEnables [Tooltip] console logging (pool get/release, show/hide, geometry repositions, coordinate math) for diagnosing positioning issues. Backed by the static Tooltip.DebugLog, so it applies to all tooltips while set.

Show/hide transitions (juice)

When show-transition / hide-transition are set, tooltip items animate with designer-authored sequence assets instead of the built-in opacity fade — pop-in with scale, directional slides, whatever the asset describes. Asset paths in UXML must be relative to the .uxml file:

xml
<CupkekGames.Luna.Tooltip name="MyTooltip" picking-mode="Ignore" show-transition="Presets/TooltipShowPop.asset" hide-transition="Presets/TooltipHideFade.asset"/>

Authoring rules for these assets:

  • Both should use Persist — their end state (fully shown / fully hidden) is the point. The show asset should open with instant SetOpacity(0) / SetScale(0.9) steps so a reused pooled item always starts from the hidden pose.
  • The OnFadeInStart / OnFadeIn / OnFadeOutStart / OnFadeOut item events fire the same as with the built-in fade, so lock/level/pool behavior is unchanged.
  • Re-showing during a hide cancels the pending hide cleanly (the item never gets stuck hidden).
  • If neither attribute is set, behavior is exactly the pre-existing FadeDuration fade — zero migration needed.
  • Positioning is computed from the item's untransformed layout size, so scale/translate transitions never affect where the tooltip lands — it is placed before the show transition starts (when the pooled item already has a layout) and re-placed automatically whenever its content size changes.
  • Anchored (non-tooltip-follow) tooltips reposition only on real geometry changes of the item or the target — never on mouse move — so an in-flight transition on either element can't make the tooltip twitch. Only tooltip-follow tooltips track the pointer.

CSS classes

Add these to the target element (the one that should display a tooltip), not to the tooltip itself.

Position

Default position is top.

ClassEffect
tooltip-leftPosition the tooltip to the left of the target.
tooltip-rightPosition the tooltip to the right of the target.
tooltip-bottomPosition the tooltip below the target.

Follow mouse

ClassEffect
tooltip-followPosition the tooltip relative to the cursor and follow it. Position classes still apply.

Styling the card

Backgrounds: flat vs effect

The tooltip card has two background paths:

  • Flat — pass colorBackground / colorBorder and the card is painted with bg-* / border-* utility classes. Zero extra elements; this is the default path (falls back to scarlet-900 / coral-400 when you pass nothing).
  • Effect — pass any VisualElement as absoluteBackground. It's injected into a dedicated absolute layer that fills the card behind the content and is clipped to the card's rounded corners — so gradients, 9-sliced frames, or shader effects (e.g. a luna-fx luna-fx-matte element) render as the card surface without ever touching the title/body layout. Pair it with colorBackground: new UIColor("transparent", ...) if you don't want the flat fill underneath.
csharp
// A custom surface behind the tooltip content (see the Showcase RPGDemo) VisualElement fx = new VisualElement(); fx.AddToClassList("rpg-tooltip-background"); // styled in your USS var setup = new TooltipContainerSetup(icon, title, body, bottom, colorBackground: null, colorBorder: null, maxWidth: 512, absoluteBackground: fx);

The layer's positioning (absolute overlay + stretched child) is structural and ships in the runtime library (LibTooltip.uss), so it works under any theme. Themes are free to restyle the layer itself — the RPGDemo sample insets it 8 px and paints a 9-sliced border texture on it (RPGTooltip.uss).

Custom classes and pooling

Tooltip items are pooled and reused across unrelated setups, so the system clears class lists on some elements at every show to prevent style leaks. Know which elements keep a manually added class:

ElementClasses survive re-show?
Elements you pass in (image, title, body, bottom, absoluteBackground)✅ — they're your objects; style them freely before handing them over.
Built-in slots (TooltipTitle, TooltipBody, TooltipAbsoluteBackground, …)✅ — never cleared, only their children are replaced.
The container card❌ — ClearClassList() runs every show; only the colorBackground/colorBorder classes are re-applied.
The root .tooltip_item element❌ — reset on every Show().

To put a persistent custom look on the card itself, go through the UIColor channel: UIColor.Name is a free string and the applied class is simply bg-<name>-<value> (or border-<name>-<value>). Define that class in your USS — it may contain any properties, not just a color — and it's re-applied after the clear on every show:

csharp
// class "bg-mystyle-900" — define it yourself in USS var setup = new TooltipContainerSetup(icon, title, body, bottom, colorBackground: new UIColor("mystyle", UIColorValue.V_900));
css
.bg-mystyle-900 { background-color: rgba(10, 8, 24, 0.95); border-radius: 16px; } .bg-mystyle-900 .tooltip_title Label { color: gold; } /* descendant selectors reach everything */

API

TooltipController

Owns the Tooltip element living in its GameObject's PanelRenderer tree. Built on PanelRendererBinder, so Tooltip stays null until the panel delivers its tree — consumers (like TooltipManipulator) null-guard on it.

csharp
TooltipController.Tooltip.SetTooltipEnabled(false); // mute all tooltips TooltipController.Tooltip.SetTooltipEnabled(true); // resume

Useful during drag operations, cutscenes, or when another UI element should have focus.

Tooltips silently not showing? If the controller's panel tree contains no <luna:Tooltip> element, every hover no-ops — the controller logs a warning when this happens. Check the PanelRenderer's sourceAsset on the TooltipController's GameObject (it must be the UXML that contains the Tooltip element, e.g. DefaultTooltip.uxml).

TooltipManipulator

Tooltip Compare

csharp
// Multi-column variant (item-vs-equipped comparison) public TooltipManipulator(GameObject parent, TooltipController tooltipController, List<TooltipContainerSetup> setups); // Single-column variant public TooltipManipulator(GameObject parent, TooltipController tooltipController); manipulator.SetSetup(setup);
ParameterTypeDescription
parentGameObjectTooltip auto-closes when this object is disabled.
tooltipControllerTooltipControllerReference to the scene's controller.
setup(s)TooltipContainerSetupPer-column container setup.

TooltipContainerSetup

csharp
public TooltipContainerSetup( VisualElement image, VisualElement title, VisualElement body, VisualElement bottom, UIColor colorBackground = null, UIColor colorBorder = null, int maxWidth = 512, VisualElement absoluteBackground = null )
ParameterTypeDescription
imageVisualElementImage slot (e.g. item icon).
titleVisualElementTitle slot.
bodyVisualElementBody slot.
bottomVisualElementFree-form bottom slot — use this when you don't want the prior row layout.
colorBackgroundUIColorContainer background tint.
colorBorderUIColorContainer border tint.
maxWidthintMax container width in pixels (default 512).
absoluteBackgroundVisualElementOptional element rendered as an absolute background behind the container — see Styling the card.

Example

PanelRenderer delivers its visual tree asynchronously — query the target element in the reload callback, not in Awake/Start:

csharp
using UnityEngine; using UnityEngine.UIElements; using CupkekGames.Luna; public class MyTooltipSetup : MonoBehaviour { [SerializeField] private TooltipController _tooltipController; [SerializeField] private Sprite _mySprite; private PanelRenderer _panelRenderer; private void Awake() { _panelRenderer = GetComponent<PanelRenderer>(); _panelRenderer.RegisterUIReloadCallback(OnUIReload); } private void OnDestroy() { _panelRenderer.UnregisterUIReloadCallback(OnUIReload); } private void OnUIReload(PanelRenderer renderer, VisualElement root, int version) { VisualElement tooltipOnHover = root.Q<VisualElement>("TooltipOnHover"); VisualElement image = new VisualElement(); image.AddToClassList("size-40"); image.style.backgroundImage = new StyleBackground(_mySprite); Label title = new Label("My Tooltip Title"); Label body = new Label("My Tooltip Description"); VisualElement bottom = null; UIColor bgColor = new UIColor("slate", UIColorValue.V_800); UIColor borderColor = new UIColor("slate", UIColorValue.V_200); var containerSetup = new TooltipContainerSetup(image, title, body, bottom, bgColor, borderColor); TooltipManipulator manipulator = new TooltipManipulator(gameObject, _tooltipController); manipulator.SetSetup(containerSetup); tooltipOnHover.AddManipulator(manipulator); } }

See also

Settings

Theme

Light

Contrast

Material

Dark

Dim

Material Dark

System

Sidebar(Light & Contrast only)

Light
Dark

Font Family

DM Sans

Wix

Inclusive Sans

AR One Sans

Direction

LTR
RTL