Tooltip
A versatile tooltip component that displays helpful information when hovering over or focusing on elements. Supports text content, custom elements, keyboard shortcuts, different trigger methods, and positioning options.
#Mobile behavior
On iOS and Android, tooltips managed by the GPUI Base overlay are disabled. Shared components may keep their tooltip configuration, but mobile actions still need visible or accessible labels. Direct GPUI .tooltip() calls, including the basic div() example below, bypass this overlay and are not disabled by this policy.
#Import
use gpui_kit::component::tooltip::Tooltip;// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { Placement, Tooltip } from "tooltip.slint";use iced::widget::{column, container, row, scrollable, svg, text};
use iced::{Alignment, Element, Length, Padding, Task, Theme};#Usage
#Hover the button
Button::new("save-btn")
.label("Save")
.tooltip("Save the current document")Tooltip {
text: "Create a new document";
Button {
text: "New file";
icon: "file";
}
}// The label waits out Slint's `delay` (400ms) and cancels if the pointer
// leaves first. Its preferred side is the top, and at the top of the page
// there is room for it — the panel draws above the button rather than
// flipping.
let hover: Element<'_, Message> = tip(
line("Create a new document"),
Button::new("New file")
.icon(glyph(FILE, 16.0, |tokens| tokens.foreground))
.outline(),
)
.into();
let hover_section = column![
hover,
muted_line("The pointer rests 400ms; leaving first cancels the label."),
]
.spacing(8.0);#Basic Tooltip with Text
// Simple text tooltip
div()
.child("Hover me")
.id("basic-tooltip")
.tooltip(|window, cx| {
Tooltip::new("This is a helpful tooltip").build(window, cx)
})HorizontalLayout {
spacing: 16px;
alignment: center;
Tooltip {
text: "Bold (⌘B)";
Button {
text: "Bold";
}
}
Tooltip {
text: "Italic (⌘I)";
Button {
text: "Italic";
}
}
Tooltip {
text: "Underline (⌘U)";
placement: Placement.bottom;
Button {
text: "Underline";
}
}
}// One label per button, and the third asks for the side under its trigger.
let basic = row![
tip(line("Bold (Cmd B)"), Button::new("Bold").outline()),
tip(line("Italic (Cmd I)"), Button::new("Italic").outline()),
tip(line("Underline (Cmd U)"), Button::new("Underline").outline())
.placement(Side::Bottom),
]
.spacing(16.0)
.align_y(Alignment::Center);
let basic_section = body_box(basic);#Tooltip with Action/Keybinding
actions!(my_actions, [SaveDocument]);
Button::new("save-btn")
.label("Save")
.tooltip_with_action(
"Save the current document",
&SaveDocument,
Some("MyContext")
)Tooltip {
text: "Save the workspace — ⌘S";
Button {
text: "Save";
icon: "download";
}
}// GPUI's `Tooltip` can resolve the keybinding of a named action. iced has no
// action registry a component can ask, so the shortcut is part of the label
// — which is what the Slint component does with the same card.
let keybinding: Element<'_, Message> = tip(
line("Save the workspace — Cmd S"),
Button::new("Save")
.icon(glyph(DOWNLOAD, 16.0, |tokens| tokens.foreground))
.outline(),
)
.into();
let keybinding_section = body_box(keybinding);#Custom Element Tooltip
div()
.child("Hover for rich content")
.id("rich-tooltip")
.tooltip(|window, cx| {
Tooltip::element(|_, cx| {
h_flex()
.gap_x_1()
.child(IconName::Info)
.child(
div()
.child("Muted Text")
.text_color(cx.theme().muted_foreground)
)
.child(
div()
.child("Danger Text")
.text_color(cx.theme().danger)
)
.child(IconName::ArrowUp)
})
.build(window, cx)
})// The label is an element, so it is not limited to one line: this one is a
// small column with a glyph, two muted lines and the `Kbd`-shaped shortcut
// the page describes, built from the same tokens as the rest of the card.
let rich_label = column![
row![
glyph(FILE, 14.0, |tokens| tokens.muted_foreground),
text("document.txt")
.size(theme::FONT_SIZE_SM)
.font(iced::Font {
weight: iced::font::Weight::Medium,
..iced::Font::DEFAULT
}),
]
.spacing(theme::SPACING_XS)
.align_y(Alignment::Center),
muted_line("Size: 2.4 KB"),
muted_line("Modified: 2 hours ago"),
row![
container(
text("Enter")
.size(theme::FONT_SIZE_XS)
.font(theme::MONO)
)
.padding(Padding::from([1.0, 6.0]))
.style(|theme: &Theme| container::Style {
background: Some(Tokens::of(theme).muted.into()),
border: iced::border::rounded(theme::RADIUS_SM)
.color(Tokens::of(theme).border)
.width(1.0),
..container::Style::default()
}),
muted_line("to open"),
]
.spacing(theme::SPACING_XS)
.align_y(Alignment::Center),
]
.spacing(4.0);
let rich = tip(
rich_label,
container(text("File: document.txt").size(theme::FONT_SIZE_SM))
.padding(Padding::from([8.0, 12.0]))
.style(|theme: &Theme| container::Style {
border: iced::border::rounded(theme::RADIUS_MD)
.color(Tokens::of(theme).border)
.width(1.0),
..container::Style::default()
}),
)
.arrow(Arrow::Center);
let rich_section = body_box(rich);#Tooltip with Manual Keybinding
div()
.child("Custom keybinding")
.id("custom-kb")
.tooltip(|window, cx| {
Tooltip::new("Delete item")
.key_binding(Some(Kbd::new("Delete")))
.build(window, cx)
})#Toolbar with Tooltips
h_flex()
.gap_1()
.child(
Button::new("new")
.icon(IconName::Plus)
.tooltip_with_action("Create new file", &NewFile, Some("Editor"))
)
.child(
Button::new("open")
.icon(IconName::FolderOpen)
.tooltip_with_action("Open file", &OpenFile, Some("Editor"))
)
.child(
Button::new("save")
.icon(IconName::Save)
.tooltip_with_action("Save file", &SaveFile, Some("Editor"))
)Rectangle {
horizontal-stretch: 1;
border-radius: Theme.radius-md;
border-width: 1px;
border-color: Theme.border;
background: Theme.sidebar;
HorizontalLayout {
padding: 6px;
spacing: 4px;
alignment: start;
Tooltip {
text: "Undo";
Button {
icon: "arrow-left";
}
}
Tooltip {
text: "Redo";
Button {
icon: "arrow-right";
}
}
Rectangle {
width: 1px;
height: 20px;
background: Theme.border;
}
Tooltip {
text: "Zoom in";
Button {
icon: "plus";
}
}
Tooltip {
text: "Zoom out";
Button {
icon: "minus";
}
}
}
}// A tooltip is a label, not a menu: it never takes the click, so the pointer
// passes straight through it to the control it describes.
let toolbar = container(
row![
icon_button(ARROW_LEFT, "Undo"),
icon_button(ARROW_RIGHT, "Redo"),
divider(),
icon_button(PLUS, "Zoom in"),
icon_button(MINUS, "Zoom out"),
]
.spacing(4.0)
.align_y(Alignment::Center),
)
.padding(6.0)
.width(Length::Fill)
.style(|theme: &Theme| {
let tokens = Tokens::of(theme);
container::Style {
background: Some(tokens.sidebar.into()),
border: iced::border::rounded(theme::RADIUS_MD)
.color(tokens.border)
.width(1.0),
..container::Style::default()
}
});
let toolbar_section = body_box(toolbar);#Status Indicators with Tooltips
h_flex()
.gap_2()
.child(
div()
.size_3()
.rounded_full()
.bg(cx.theme().success)
.tooltip(|window, cx| {
Tooltip::new("Connected to server").build(window, cx)
})
)
.child(
div()
.size_3()
.rounded_full()
.bg(cx.theme().warning)
.tooltip(|window, cx| {
Tooltip::new("Limited connectivity").build(window, cx)
})
)HorizontalLayout {
spacing: 20px;
alignment: center;
Tooltip {
text: "Build passed 4 minutes ago";
Icon {
name: "circle-check";
size: 18px;
color: Theme.success;
}
}
Tooltip {
text: "2 warnings in the last build";
Icon {
name: "triangle-alert";
size: 18px;
color: Theme.warning;
}
}
Tooltip {
text: "Upload failed";
Icon {
name: "circle-x";
size: 18px;
color: Theme.danger;
}
}
}// Any hoverable element can be the host, including a bare glyph: the trigger
// is whatever element the tooltip was built around.
let status = row![
tip(
line("Build passed 4 minutes ago"),
glyph(CIRCLE_CHECK, 18.0, |tokens| tokens.success)
),
tip(
line("2 warnings in the last build"),
glyph(TRIANGLE_ALERT, 18.0, |tokens| tokens.warning)
),
tip(line("Upload failed"), glyph(CIRCLE_X, 18.0, |tokens| tokens.danger)),
]
.spacing(20.0)
.align_y(Alignment::Center);
let status_section = body_box(status);#Interactive Elements with Rich Tooltips
v_flex()
.gap_3()
.child(
div()
.p_2()
.border_1()
.border_color(cx.theme().border)
.rounded(cx.theme().radius)
.child("File: document.txt")
.id("file-item")
.tooltip(|window, cx| {
Tooltip::element(|_, cx| {
v_flex()
.gap_1()
.child(
h_flex()
.gap_2()
.child(IconName::File)
.child("document.txt")
.text_sm()
.font_medium()
)
.child(
div()
.child("Size: 2.4 KB")
.text_xs()
.text_color(cx.theme().muted_foreground)
)
.child(
div()
.child("Modified: 2 hours ago")
.text_xs()
.text_color(cx.theme().muted_foreground)
)
.child(
h_flex()
.gap_1()
.child(Kbd::new("Enter"))
.child("to open")
.text_xs()
.text_color(cx.theme().muted_foreground)
)
})
.build(window, cx)
})
)#Form Validation with Tooltips
// A field with help over it. GPUI's page swaps the label for the validation
// error when there is one; an iced label is an element, so the same swap is
// a different element in the same place.
let invalid = app.email.contains(' ') || app.email.is_empty();
let field = Input::new("email")
.placeholder("Email address")
.value(app.email.clone())
.invalid(invalid)
.on_input(Message::EmailChanged);
let help = tip(
if invalid {
line("Enter an address with no spaces.")
} else {
line("We'll never share your email address.")
},
field,
);
let form_section = body_box(
column![help, muted_line("Type, then hover the field to read the help.")].spacing(8.0),
);#Arrow
struct FormView {
email_error: Option<String>,
password_error: Option<String>,
}
v_flex()
.gap_4()
.child(
Input::new("email")
.placeholder("Email address")
.when_some(self.email_error.clone(), |this, error| {
this.tooltip(move |window, cx| {
Tooltip::element(|_, cx| {
h_flex()
.gap_1()
.child(IconName::AlertCircle)
.child(error.clone())
.text_color(cx.theme().destructive)
})
.build(window, cx)
})
})
)// The Slint component has no arrow property and its example never shows
// one, so the plain cards above draw none. This is the port's own addition:
// the same surface, a triangle in its own fill between it and the trigger.
let arrows = row![
tip(line("Arrow::Start"), Button::new("Start").outline()).arrow(Arrow::Start),
tip(line("Arrow::Center"), Button::new("Center").outline()).arrow(Arrow::Center),
tip(line("Arrow::End"), Button::new("End").outline()).arrow(Arrow::End),
]
.spacing(24.0)
.align_y(Alignment::Center);
let arrow_section = body_box(arrows);#Advanced Usage
#Components with Built-in Tooltip Support
Many components have built-in tooltip methods:
// Button
Button::new("btn")
.label("Click me")
.tooltip("This button performs an action")
// Switch
Switch::new("toggle")
.label("Enable notifications")
.tooltip("Toggle push notifications on/off")
// Checkbox
Checkbox::new("check")
.label("Remember me")
.tooltip("Keep me logged in for 30 days")
// Radio
Radio::new("option")
.label("Option 1")
.tooltip("Select this option to enable feature X")#Complex Tooltip Content
div()
.child("Hover for details")
.id("complex-tooltip")
.tooltip(|window, cx| {
Tooltip::element(|_, cx| {
v_flex()
.gap_2()
.child(
h_flex()
.gap_1()
.child(IconName::User)
.child("User Information")
.text_sm()
.font_semibold()
)
.child(
div()
.child("Last login: 2 hours ago")
.text_xs()
.text_color(cx.theme().muted_foreground)
)
.child(
div()
.child("Status: Active")
.text_xs()
.text_color(cx.theme().success)
)
})
.build(window, cx)
})#Tooltip in Form Elements
v_flex()
.gap_4()
.child(
Input::new("email")
.placeholder("Enter your email")
.tooltip("We'll never share your email address")
)
.child(
Input::new("password")
.input_type(InputType::Password)
.placeholder("Password")
.tooltip("Must be at least 8 characters with special characters")
)#Best Practices
#Content Guidelines
- Be concise: Keep tooltip text short and to the point
- Be helpful: Provide additional context, not redundant information
- Use proper tone: Match your application’s voice and tone
- Avoid critical info: Don’t put essential information only in tooltips
#Usage Guidelines
- Progressive disclosure: Use tooltips for additional context, not primary information
- Consistency: Use consistent tooltip patterns throughout your application
- Performance: Avoid complex content in frequently triggered tooltips
- Testing: Test tooltips with both mouse and keyboard interaction
#Examples of Good Tooltip Content
// Good: Provides helpful context
Button::new("delete")
.icon(IconName::Trash)
.tooltip("Delete this item permanently")
// Good: Explains abbreviation
div()
.child("CPU: 45%")
.tooltip("Central Processing Unit usage")
// Good: Describes action with keybinding
Button::new("undo")
.icon(IconName::Undo)
.tooltip_with_action("Undo last action", &Undo, Some("Editor"))#Examples to Avoid
// Avoid: Redundant information
Button::new("save")
.label("Save")
.tooltip("Save") // Doesn't add value
// Avoid: Critical information
Button::new("delete")
.tooltip("This will permanently delete all your files") // Too important for tooltip only#API Reference
#Tooltip
| Method | Description |
|---|---|
new(text) | Create a tooltip with text content |
element(builder) | Create a tooltip with custom element content |
action(action, context) | Set action to display keybinding information |
key_binding(kbd) | Set manual keybinding information |
build(window, cx) | Build and return the tooltip as AnyView |
#Built-in Tooltip Methods
Components with tooltip support typically provide these methods:
| Method | Description |
|---|---|
tooltip(text) | Add simple text tooltip |
tooltip_with_action(text, action, context) | Add tooltip with action keybinding |
tooltip(closure) | Add custom tooltip with builder closure |
#Tooltip Styling
The tooltip automatically applies theme-appropriate styling:
- Background:
theme.popover - Text color:
theme.popover_foreground - Border:
theme.border - Shadow: Medium drop shadow
- Border radius: 6px
- Font: System UI font
You can apply additional styling using the Styled trait:
Tooltip::new("Custom styled tooltip")
.bg(cx.theme().accent)
.text_color(cx.theme().accent_foreground)
.build(window, cx)