A small floating callout anchored to a target with a directional pointer; ships in eight pointer positions.
Unify 3 Tooltip siblings into one Tooltip component; drop the "V2" suffix
This component, Onboarding - Tooltip, and Tooltip Blurred and Transparent model the same primitive with different skins. Merge into one
Tooltip with placement: .top | .right | .bottom | .left (one enum, not 4 booleans), appearance: .default | .onboarding | .translucent, hasArrow, hasDismiss, and a content slot. Replace the raster pointer with a vector, the placeholder icon circle with a Figma Slot, and strip the V2 suffix — production component names should never carry a version. Maps cleanly to TipKit / PlainTooltip / RichTooltip on native.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. |
Open Issues
- Component is named
Tooltip V2. Version suffixes don't belong in production DS component names — they imply a V1 that wasn't removed, and force consumers to choose which version is "right". Rename toTooltipand delete V1 (or, if V1 is still in use, merge/deprecate before renaming). C2 · Variant & Property Naming - Three sibling Tooltip components for one primitive.
Tooltip V2(70:14908),Onboarding - Tooltip(51:17066), andTooltip Blurred and Transparent(49:335349) all model the same floating popover with different skins. Collapse into oneTooltipwith anappearanceenum. C1 · Layer Structure & Naming - Pointer direction is 4 independent booleans.
pointerTop,pointerRight,pointerBottom,pointerLeft— nothing prevents a consumer from enabling two or all four. A singleplacement: .top | .right | .bottom | .left | .noneenum is the correct shape. Maps 1:1 toTipKit.arrowEdgeand ComposeTooltipAnchorPosition. C2 · Variant & Property Naming - Pointer triangle is a raster asset. 4 separate image fills (
imgPointer,imgPointer1,imgPointer2,imgPointer3) for what should be one vector shape rotated per edge. Replace with a single vector triangle component; rotation handled by theplacementenum. C6 · Asset & Icon Quality - Leading icon is a gray placeholder circle. The
Icon=yesvariant renders a flat#C2C6CF46 px circle under a "Placeholder" frame — same anti-pattern as Action List / List Item. Replace with a namedleadingFigma Slot so consumers can drop in an Icon, Avatar, or illustration. C6 · Asset & Icon Quality - Close (X) is an image asset, not a DS icon instance. The dismiss control uses
imgShapeFullinside a generic "Close" frame rather than an instance of the DS'sicon/close. That hides the icon from a11y / token updates and blocks Code Connect from seeing it as a real control. C1 · Layer Structure & Naming - CTA Button padding drifts between variants.
CTA=one, Header=yesusespx-16 / py-12;CTA=one, Header=nousespx-12 / py-6;CTA=twouses yet another combo. The underlyingButton - XSmallinstance should be identical across all variants — same size mode, same padding. C4 · Native Mappability - No dismiss / show states modeled. Close button exists visually but carries no interaction property; there is no Pressed / Focused state on the dismiss control or on CTAs at the tooltip level. Tooltips also have an implicit "appearing / dismissing" lifecycle that isn't documented. C5 · Interaction State Coverage
- Code Connect mappings not registered. Blocked on the consolidation + slot adoption + enum conversion. Mapping the current 3-sibling / 4-boolean shape to native would cement the wrong schema. C7 · Code Connect Linkability
Design Recommendations
- Consolidate the 3 Tooltip siblings into one component. New schema:
placement: .top | .right | .bottom | .left(replaces the 4 booleans),appearance: .default | .onboarding | .translucent(replaces the 3 sibling components),hasArrow: Bool,hasDismiss: Bool,cta: .none | .primary | .primaryAndSecondary, plus aleadingslot for icon/avatar and a content slot for the body. Replaces today's8 + ? + ?variants across 3 components with roughly4 placement × 3 appearance × 3 cta = 36permutations of one clean schema. Family - Rename
Tooltip V2→Tooltip. Version suffixes shouldn't appear in production DS names. If V1 is still referenced anywhere, migrate its instances first, then delete V1, then drop the suffix. Rename - Replace 4 pointer booleans with a single
placementenum. Prevents nonsensical states (all 4 pointers on), maps 1:1 to SwiftUI.arrowEdgeand ComposeTooltipAnchorPosition, and reduces the variant matrix dramatically. Property - Replace the raster pointer with a vector triangle. Today there are 4 separate rasters (top/right/bottom/left). One vector shape, rotated per
placement, fills the same role with zero asset burden, scales cleanly, and picks up token color updates automatically. Asset - Adopt a
leadingFigma Slot for the icon. Drop the#C2C6CFplaceholder circle. Maps to@ViewBuilder(SwiftUI) and a@Composableslot (Compose). Empty slot = no leading. Slot - Instance-swap the close button to
icon/close. Use the canonical DS close-icon instance rather than an inlineimgShapeFull. Gets you token-driven color, a11y labeling, and press-state handling for free. Composition - Normalize CTA Button padding. Every tooltip variant uses the same Button size mode (XSmall). Enforce one padding via the Button component itself — don't let the tooltip override. Composition
- Document the dismiss contract + lifecycle. Add a description on the component: "Tap the close X or tap outside to dismiss, unless
hasDismiss = false(force interaction with CTA). Tooltip appears with fade + slight scale from the pointer anchor; dismisses with reverse." Close the gap between designer intent and dev implementation. Docs - Add Pressed / Focused on the close control and CTAs. Inner Button instances handle their own states, but the close X does not — it's an image. Once it becomes an icon-button instance, states follow. State
Styles
Tooltip v2 variations
DES DEV
Full-shape onboarding variant. Leading icon placeholder + header + description + primary CTA + close. 359 × 181.
Content
Header
Description
Icon
CTA
Placement
Pointer
Properties
cta one
icon yes
description true
header true
Variant Hero — full content
Colors
Surface #FFFFFF
Border #E5EBF4
Header #0A2757
Description #6780A9
Close icon #0A2757
Primary CTA bg #005CE5
Primary CTA label #FFFFFF
Secondary CTA #005CE5
Layout
Width 296px (max)
Padding 16 horizontal · 12 vertical
Border radius radius/radius-2 (6px)
Border 1px solid #E5EBF4
Gap (header ↔ desc) 4px (space/space-4)
Pointer size 12 × 8 (width × height)
Typography
Header style Primary/Headlines/Block
Header font Proxima Soft Bold · 18 / 23 · +0.25
Description style Secondary/Bold/Caption
Description font BarkAda Semibold · 12 / 18
CTA style Primary/Label/Base
CTA font Proxima Soft Bold · 16 / 16 · +0.25
Colors by State
| Role | Token | Default |
|---|---|---|
| Surface | main/nudge/color/primary/bg | #FFFFFF |
| Border | main/nudge/color/primary/border | #E5EBF4 |
| Header label | main/nudge/color/primary/label | #0A2757 |
| Description | main/nudge/color/primary/description | #6780A9 |
| Close icon | main/nudge/color/primary/icon-close | #0A2757 |
| Leading icon placeholder | — | #C2C6CF (not tokenized — placeholder) |
| CTA primary bg | main/button/primary/brand/enabled/bg | #005CE5 |
| CTA primary label | main/button/primary/brand/enabled/label | #FFFFFF |
| CTA secondary border / label | main/button/secondary/brand/enabled/border | #005CE5 |
| Pointer triangle | — | raster (4 images) |
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 |
1.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