A small floating callout anchored to a target with a directional pointer; ships in eight pointer positions.
Superseded by Tooltip
Superseded by Tooltip. The version suffix is gone, the four pointer booleans became a single
Placement enum, the leading placeholder and CTA moved into ⤷ AssetSlot and ⤷ ActionSlot, and the raster pointer and close image were replaced with a vector and a DS icon instance. Kept as a record of the assessment that drove the consolidation.In Context
Tooltips sit over a target element (tab, button, icon, card) with a pointer aimed at the thing they describe.
Live Preview
Header
Description goes here. This is the second sentence. The third sentence.
Next
Content
header
description
icon
cta
Placement
pointer
DS Health
Reusable
Partial
Covers the main in-product tooltip pattern across onboarding, tips, and nudges. But because 3 siblings exist for 3 skins, consumers have to hunt for the right one — the primitive is fragmented. C1
Self-contained
Warn
Surface, type, and spacing bind to
main/nudge/* + space/* tokens. But the pointer arrow is a raster image (4 rotated copies of one shape) and the leading icon is a gray placeholder circle, not a slot. C6Consistent
Warn
Component is named
Tooltip V2 — version suffixes don't belong in production DS names. Pointer direction is 4 separate booleans rather than one enum, letting consumers set nonsensical combinations (e.g. all 4 pointers on). C2Composable
Warn
No Figma Slot for leading icon or body content. CTAs are baked Button instances that re-implement pill padding rather than composing the Button component consistently across variants (one variant uses px-16 / py-12, another uses px-8 / py-6). C4
Behavior
| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Show / hide | Yes | Yes | Not annotated | Expected: fade + slight scale-in anchored on the pointer side. |
| Tap close (X) | Yes | Yes | Close layer | Dismiss icon is present in markup but not wired to a property or interaction. Contract should be explicit. |
| Tap outside | N/A | N/A | Not defined | Standard tooltip contract — tap-outside dismisses. Should be documented on the component. |
| Primary CTA | Yes | Yes | CTA=one / two | Advances in onboarding or performs the tip's primary action. |
| Secondary CTA | Yes | Yes | CTA=two | "Back" in onboarding; "Learn more" in tips. |
| Pressed / Focused | N/A | N/A | Not built | CTAs and close have no pressed/focused treatment on the tooltip component itself. Inner Button instances handle their own press, but close + the tooltip surface do not. |
Installation
Planned API
iOS — Swift Package Manager
// In Xcode: File → Add Package Dependencies "https://github.com/AY-Org/eb-ds-ios"
Android — Gradle (Kotlin DSL)
dependencies { implementation("com.eastblue.ds:tooltip:1.0.0") }
Property Mapping
| Figma Property | SwiftUI | Compose |
|---|---|---|
| 3 sibling components | 1 component: Tooltip | EBTooltip |
| (sibling = appearance) | appearance: .default / .onboarding / .translucent | .ebAppearance(.default / .onboarding / .translucent) |
| pointerTop/Right/Bottom/Left: Bool × 4 | placement: .top / .right / .bottom / .left / .none | arrowEdge: Edge |
| header: Bool + text baked | title: String? | title: String? |
| description: Bool + text baked | body: String? (or content slot) | body: String? |
| icon: Bool (gray placeholder) | leading (Slot) | @ViewBuilder leading |
| Close image asset (always) | hasDismiss: Bool | dismissible: Bool |
| cta: none / one / two | cta: .none / .primary(String) / .pair(back, next) | primary / secondary: TooltipAction? |
| outlineButton: Bool | (absorbed into cta.pair) | — |
| (not modeled) | hasArrow: Bool | arrow: Bool |
| (not modeled) | onDismiss | onDismiss: () -> Void |
SwiftUI
ios/Components/Tooltip/EBTooltip.swift
Jetpack Compose
android/components/tooltip/EBTooltip.kt
Usage Snippets Planned API
Usage
// Simple tip — title + body, pointer below EBTooltip( title: "Quick transfers", body: "Tap here to send money to recent contacts.", placement: .bottom ) .onDismiss { showTip = false } // Walkthrough step — two CTAs EBTooltip( title: "Step 2 of 4", body: "Review your balance before confirming.", placement: .top, cta: .pair(back: "Back", next: "Next") ) .ebAppearance(.onboarding) // Rich — custom leading slot EBTooltip(title: "Welcome", body: "Tap a card to begin.") { Image(systemName: "sparkles") } // iOS 17 + Tips — recommended for onboarding flows .popoverTip(quickTransferTip)
// Simple tip EBTooltip( title = "Quick transfers", body = "Tap here to send money to recent contacts.", placement = EBTooltipPlacement.Bottom, onDismiss = { showTip = false } ) // Walkthrough step — two CTAs, onboarding skin EBTooltip( title = "Step 2 of 4", body = "Review your balance before confirming.", placement = EBTooltipPlacement.Top, appearance = EBTooltipAppearance.Onboarding, primaryAction = TooltipAction("Next", onClick = { advance() }), secondaryAction = TooltipAction("Back", onClick = { rewind() }) ) // Custom leading slot EBTooltip( title = "Welcome", body = "Tap a card to begin.", leading = { Icon(Icons.Default.AutoAwesome, contentDescription = null) } ) // Material 3 PlainTooltip equivalent PlainTooltip { Text("Shortcut") }
Accessibility
| Requirement | iOS | Android |
|---|---|---|
| Role + focus | Announce tooltip as .accessibilityAddTraits(.isModal) when hasDismiss = true — otherwise role .staticText. | semantics { role = Role.Popup } on the container; TalkBack focuses it on appear. |
| Close control | Wrap close as a Button with accessibilityLabel "Dismiss tip" and 44×44 hit target. | IconButton with contentDescription = "Dismiss tip"; 48×48dp minimum. |
| Dismiss-outside | Respect UIAccessibility.isVoiceOverRunning — do not auto-dismiss tooltips while VO is active. | Do not auto-dismiss while TalkBack is active; hold the tooltip until the user explicitly moves on. |
| Reduce motion | Respect UIAccessibility.isReduceMotionEnabled — skip the scale-in animation; fade only. | Respect Settings.Global.TRANSITION_ANIMATION_SCALE — fade only when user has motion reduced. |
| Combined label | Read title + body + "Dismiss" as one phrase; avoid reading pointer. | Same; set mergeDescendants = true on the container. |
Criteria Scorecard
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Requires Rework | 3 sibling components for one primitive. Close uses a raw image asset inside a generic frame rather than an icon/close instance. |
| C2 | Variant & Property Naming | Requires Rework | Version suffix in the component name (Tooltip V2). Pointer direction is 4 booleans instead of one enum. |
| C3 | Token Coverage | Ready | Surface, border, label, description, and CTA colors bound to main/nudge/* and main/button/* tokens. Spacing via space/*. |
| C4 | Native Mappability | Requires Rework | Maps cleanly once pointer booleans → placement enum and sibling skins → appearance enum. CTA padding inconsistencies block a 1:1 Button reuse. |
| C5 | Interaction State Coverage | Requires Rework | No Pressed / Focused on close. No lifecycle (appearing / dismissing) annotated. Close isn't wired to a dismiss property. |
| C6 | Asset & Icon Quality | Requires Rework | Pointer is 4 raster images (one per edge). Leading icon is a gray placeholder circle. Close is an image asset. |
| C7 | Code Connect Linkability | Not Mapped | Blocked on consolidation + enum conversion + slot adoption. Mapping today's shape would cement the wrong schema. |
Variants Inventory (8 total)
4 booleans and one 3-value enum yield 8 shipped variants out of a 24-cell theoretical matrix (CTA (3) × Icon (2) × Description (2) × Header (2) = 24). Only the 8 meaningful combinations are exposed. Pointer direction is 4 additional booleans outside the variant matrix.
| # | CTA | Icon | Description | Header | Dimensions | Node |
|---|---|---|---|---|---|---|
| 1 | one | yes | yes | yes | 359 × 181 | 70:14907 |
| 2 | one | no | yes | yes | 359 × 155 | 7977:12260 |
| 3 | none | yes | yes | yes | 359 × 137 | 70:14903 |
| 4 | none | no | yes | yes | 359 × 119 | 70:14902 |
| 5 | none | no | no | yes | 359 × 79 | 70:14900 |
| 6 | none | no | yes | no | 359 × 92 | 70:14901 |
| 7 | two | no | yes | no | 359 × 136 | 70:14905 |
| 8 | one | no | yes | no | 359 × 136 | 70:14906 |
2.0.0 — August 2026Major
Consolidated into Tooltip · node 6295:79647
Superseded by Tooltip —
ConsolidatedTooltip V2 was consolidated into the unified Tooltip set (node 6295:79647) alongside its two siblings. Verdict changed to Remove; open issues and recommendations cleared, since they are resolved on the successor.
Resolved1.0.0 — April 2026Major
Initial Assessment · node 70:14908
Verdict: Restructure — Consolidate 3 sibling Tooltip components; drop the
ArchitectureV2 suffix; replace 4 pointer booleans with one placement enum; replace raster pointer with vector; replace placeholder icon with a Figma Slot. OpenC1 — 3 siblings for 1 primitive — Tooltip V2, Onboarding - Tooltip, Tooltip Blurred and Transparent. Merge via
C1appearance enum. OpenC2 — Version suffix in name —
C2Tooltip V2 shouldn't exist in production; no V1 surfaces in the file. OpenC2 — Pointer is 4 booleans — Replace
C2pointerTop/Right/Bottom/Left with a single placement enum. OpenC4 — CTA padding drift — Same XSmall Button, three different paddings across variants. Normalize. Open
C4C5 — No dismiss/focus states — Close is decorative; Pressed / Focused / Lifecycle not modeled. Open
C5C6 — Raster pointer + placeholder icon — 4 raster images for the pointer; gray circle for the leading icon. Vector + slot. Open
C6C7 — Code Connect — Blocked on the architectural changes above. Open
C7Tokens ✓ — Surface / border / label / description / CTA colors all bound to
Praisemain/nudge/* and main/button/*. Spacing via space/*. Noted