Tooltips (SusKit)
Service:
TooltipService(sus-kit/Runtime/Services/) Extension:TooltipExtensions.WithTooltip()(sus-kit/Runtime/Extensions/) Model:SusTooltipCardEntry(sus-kit/Runtime/Models/)
1. Purpose
Tooltips appear on hover over a UI element. They render through the shared portal OverlayHost (category Tooltip = 30) and are not clipped by the parent.
Two modes (plus custom cards):
- Text (
ShowAtElement) is a simple string, positioned relative to the targetVisualElement. - Rich (
ShowRich) — a card with title, description, icon (SusTooltipCardEntry). - Custom (
ShowContent) — anyVisualElementbuilt lazily viaFunc<VisualElement>, positioned like text tooltips (auto-flip + clamp). Use when the card isn'tSusTooltip/SusTooltipCard(e.g. game-specific item chrome). Also exposed asWithTooltip(Func<VisualElement>, placement).
Show delay (500ms) and hide delay (200ms) are adjustable via ShowDelayMs / HideDelayMs. Only one tooltip is active at a time — calling again cancels the previous one.
2. Architecture
Current implementation:
TooltipServiceusesOverlayHostdirectly for rich tooltips. Component tooltips (SusTooltip.sharq) useSusOverlayManager.ShowTooltip()— a centralized wrapper over OverlayHost that copies style sheets (CopyAncestorStyleSheets).
OverlayHost (portal, auto-BringToFront via GeometryChangedEvent)
├── [World = 0] healthbar / nameplate
├── [Transition = 10] curtain
├── [Modal = 20] dialogues
├── [Tooltip = 30] ← TOOLTIPS HERE (SusOverlayManager.ShowTooltip)
├── [Dropdown = 40] ← Select/Dropdown (SusOverlayManager.Show)
├── [Toast = 45] snackbars
└── [Console = 50] dev-console- Style copying:
OverlayHost.AddToOverlay()callsCopyAncestorStyleSheets(), which copies the.g.ussstyle sheets from the nearestSusComponentancestor before the element is detached withRemoveFromHierarchy(). This ensures the tooltip card keeps its styles after being teleported into the overlay. - Important:
ShowTooltip()does NOT callRemoveFromHierarchy()beforeAddToOverlay()— doing so would break the ancestor chain before the styles are copied. design-tokens.ussandsuskit-base.ussare loaded directly ontoOverlayHostitself — CSS variables and base styles are visible to every child of the overlay.
3. TooltipService API
File: sus-kit/Runtime/Services/TooltipService.cs
Initialization
var tooltipService = new TooltipService();
tooltipService.Init(overlayHost); // OverlayHost for portal rendering
TooltipService.Instance = tooltipService; // Singleton for WithTooltip
// Factories (Sharq components live in the CompiledUI assembly and are not directly accessible)
tooltipService.CreateTooltip = () => new SusTooltip();
tooltipService.CreateCard = () => new SusTooltipCard();Methods
| Method | Signature | Description |
|---|---|---|
ShowAtElement | (VisualElement target, string text, TooltipPlacement placement = Auto) | Text tooltip on an element. Delay: ShowDelayMs (default 500ms). Internally uses ShowContent + CreateTooltip. |
ShowContent | (VisualElement target, Func<VisualElement> contentFactory, TooltipPlacement placement = Auto) | Custom card on the OverlayHost Tooltip layer. Lazy factory after the show delay; same placement / auto-flip as text. |
ShowRich | (SusTooltipCardEntry entry, Vector2? screenPosition = null) | Rich card via CreateCard + Apply(entry). If position is omitted — centered on screen. |
Hide | () | Hide immediately. |
HideAfterDelay | () | Hide via HideDelayMs (default 200ms). Used on PointerLeave. |
Properties
| Property | Type | Default | Description |
|---|---|---|---|
Instance | TooltipService (static) | — | Singleton for WithTooltip |
ShowDelayMs | Prop<float> | 500 | Delay before showing (ms) |
HideDelayMs | Prop<float> | 200 | Delay before hiding (ms) |
Overlay | OverlayHost | — | Portal container |
CreateTooltip | Func<VisualElement> | — | Factory for SusTooltip (Sharq) |
CreateCard | Func<VisualElement> | — | Factory for SusTooltipCard (Sharq) |
IsVisible | bool (read-only) | — | Whether a tooltip is currently active |
4. Positioning and auto-flip
TooltipPlacement
public enum TooltipPlacement
{
Auto, // Tries from bottom → top → right → left
Top,
Bottom,
Left,
Right
}Auto algorithm
- Bottom: if
target.bottom + tooltip.height + 4px≤ panel height →Bottom. - Top: if
target.top - tooltip.height - 4px≥ 0 →Top. - Right: if
target.right + tooltip.width + 4px≤ panel width →Right. - Left: fallback.
Clamping
After the position is calculated, the tooltip is clamped to the panel edges (4px margin):
x = Mathf.Clamp(x, 4f, panelWidth - tooltipWidth - 4f);
y = Mathf.Clamp(y, 4f, panelHeight - tooltipHeight - 4f);Pure functions for tests
// Resolve placement (no panel)
TooltipPlacement resolved = TooltipService.ResolvePlacement(
targetRect, tooltipSize, panelSize, TooltipPlacement.Auto);
// Resolve position (in panel coordinates)
Vector2 pos = TooltipService.ResolvePosition(
targetRect, tooltipSize, panelSize, TooltipPlacement.Bottom);5. Fluent extension WithTooltip
File: sus-kit/Runtime/Extensions/TooltipExtensions.cs
A convenient way to attach a tooltip to any VisualElement:
// Simple text
myButton.WithTooltip("Save game");
// With explicit placement
iconButton.WithTooltip("Settings", TooltipPlacement.Right);
// Rich card
var entry = new SusTooltipCardEntry
{
Title = "Fireball",
Description = "Damage: 50-70\nRange: 3 cells\nCooldown: 2 turns",
IconAlias = "fire",
};
spellIcon.WithTooltip(entry);
// Custom VisualElement (lazy factory) — any card on the OverlayHost Tooltip layer
slot.WithTooltip(() => BuildItemTooltip(item), TooltipPlacement.Right);How it works:
PointerEnterEvent→service.ShowAtElement/ShowContent(afterShowDelayMs).PointerLeaveEvent→service.HideAfterDelay()(afterHideDelayMs).- When the element is detached (
DetachFromPanelEvent) →service.Hide().
ShowContent(target, contentFactory, placement) builds the card after the show delay and positions it like text tooltips (auto-flip + clamp).
6. SusTooltipCardEntry
File: sus-kit/Runtime/Models/SusTooltipCardEntry.cs
public class SusTooltipCardEntry
{
public string Title; // Heading
public string Description; // Description (multiline)
public string IconAlias; // Alias of an icon from SusIconRegistry, e.g. "info-circle"
public string FooterText; // Footer text
}Used with ShowRich:
service.ShowRich(new SusTooltipCardEntry
{
Title = "Sword of justice",
Description = "Attack: 15-20\nType: slashing",
IconAlias = "sword",
FooterText = "One-handed"
});7. Integration with the router
The tooltip is cleared on route change via a beforeEach guard:
// In SusRouter.Init()
ModalService.CloseAll(); // modals
TooltipService.Instance?.Hide(); // active tooltipWhen the target element is detached, the tooltip hides automatically (WithTooltip listens for DetachFromPanelEvent).
Theming (--sus-tooltip-*)
Defined in SusTooltip.sharq <style> (override in project USS on .sus-tooltip__card):
| Token | Default role |
|---|---|
--sus-tooltip-bg | Card background |
--sus-tooltip-radius | Corner radius |
--sus-tooltip-border / --sus-tooltip-border-hover | Outline |
--sus-tooltip-pad | Card padding |
--sus-tooltip-header-bg | Header strip (also set by rarity) |
Rarity modifiers on the tip + card: sus-tooltip--rarity-common|uncommon|rare|epic|legendary|exotic (set by inventory / item cards on hover). Header strip color comes from --sk-rarity-* in suskit-tokens.uss:
| Class | Token |
|---|---|
sus-tooltip--rarity-common | --sk-rarity-common |
sus-tooltip--rarity-uncommon | --sk-rarity-uncommon |
sus-tooltip--rarity-rare | --sk-rarity-rare |
sus-tooltip--rarity-epic | --sk-rarity-epic |
sus-tooltip--rarity-legendary | --sk-rarity-legendary |
sus-tooltip--rarity-exotic | --sk-rarity-exotic |
Override --sk-rarity-* (or --thm-rarity-*) in the consumer theme USS.
8. Full wiring example
// 1. Bootstrap
var overlay = SusBootstrap.GetOrCreateOverlay(uiDocument.rootVisualElement);
var tooltip = new TooltipService();
tooltip.Init(overlay);
tooltip.CreateTooltip = () => new SusTooltip();
tooltip.CreateCard = () => new SusTooltipCard();
TooltipService.Instance = tooltip;
// 2. The router picks up the same OverlayHost
router.Init(overlay);
// 3. Components use WithTooltip
settingsButton.WithTooltip("Game Settings", TooltipPlacement.Left);
attackIcon.WithTooltip(new SusTooltipCardEntry { Title = "Attack", Description = "Basic attack" });
slot.WithTooltip(() => BuildCustomCard(item), TooltipPlacement.Right);9. Tests
| File | Tests | What it covers |
|---|---|---|
sus-kit/Editor/Tests/TooltipPlacementTests.cs | 9 editmode | ResolvePlacement, ResolvePosition, auto-flip on edges |
sus-kit/Runtime/Tests/TooltipServicePlaymodeTests.cs | 4 playmode | ShowAtElement → visibility, Hide, single-tooltip rule, StartsHidden |
sus-kit/Runtime/Tests/WithTooltipTests.cs | 4 playmode | WithTooltip string/rich/null-service/null-element |
sus-kit/Editor/Tests/SusTooltipCardEntryTests.cs | ~3 editmode | SusTooltipCardEntry properties |
10. OverlayHost layers + SusOverlayManager helpers
Documented above: OverlayHost categories (Modal = 20, Tooltip = 30, Dropdown = 40, Toast = 45, Console = 50). Tooltips and dropdowns stay above modals so popups from inside a dialog remain visible.
OverlayHost is already the portal host for kit tooltips (TooltipService.Init(overlayHost) → OverlayCategory.Tooltip). In addition, SusOverlayManager keeps two convenience layers inside a panel for component-local dropdowns / tooltips:
panel.visualTree
├── ...app content...
├── sus-overlay-popup ← Dropdown/Select (Show/Hide)
└── sus-overlay-tooltip ← SusTooltip (ShowTooltip/HideTooltip)
↑ always above the popup → the tooltip is visible on top of dropdowns- Popup layer: one active at a time, click-outside, scroll tracking -
SusOverlayManager.Show/Hide - Tooltip layer:
pickingMode = Ignore, several can be active, closes on hover out -SusOverlayManager.ShowTooltip/HideTooltip - Cleanup:
DetachFromPanelEventon the activator,panelDestroyedon the panel
Mapping to OverlayHost categories (when the app uses the shared portal):
- Popup / select menus →
OverlayCategory.Dropdown = 40 - Tooltips →
OverlayCategory.Tooltip = 30 - Toasts →
OverlayCategory.Toast = 45 - The
ShowTooltip/HideTooltipAPI stays the same; the host is either OverlayHost or the manager helper.