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.

Install

#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)
    })
SlintNo Slint version of this example yet.
// 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)
    })
SlintNo Slint version of this example yet.

#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)
            })
    )
SlintNo Slint version of this example yet.

#Form Validation with Tooltips

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// 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)
                })
            })
    )
SlintNo Slint version of this example yet.
// 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")
SlintNo Slint version of this example yet.

#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)
    })
SlintNo Slint version of this example yet.

#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")
    )
SlintNo Slint version of this example yet.

#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"))
SlintNo Slint version of this example yet.

#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
SlintNo Slint version of this example yet.

#API Reference

#Tooltip

MethodDescription
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:

MethodDescription
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)
SlintNo Slint version of this example yet.

#Placement

top, bottom, left, right

#Tooltip

A delayed hover label. Uses a popup so the label is not clipped by the control it describes (reaction pills, buttons, bubbles).

NameKindTypeDefault
textinstring
placementinPlacementPlacement.top
delayinduration400ms