HoverCard

HoverCard component for displaying rich content that appears when the mouse hovers over a trigger element. Ideal for previewing user profiles, link previews, and other contextual information without requiring a click. Features configurable delays for both opening and closing to prevent flickering during quick mouse movements.

Install

This is most like the Popover component, but triggered by hover instead of click, and with timing controls for a smoother user experience.

On iOS and Android, tap the trigger to open or close the card. Tapping outside closes it; tapping inside keeps it open. Hover delays do not apply. Tooltip hints remain disabled.

#Import

use gpui_kit::component::hover_card::HoverCard;
// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { HoverCard } from "hover-card.slint";
use iced::time::Duration;
use iced::widget::{column, container, row, scrollable, svg, text};
use iced::{Alignment, Element, Length, Padding, Task, Theme};

#Usage

#Hover

use gpui_kit::{ParentElement as _, Styled as _};
use gpui_kit::component::{hover_card::HoverCard, v_flex};

HoverCard::new("basic")
    .trigger(
        div()
            .child("Hover over me")
            .text_color(cx.theme().primary)
            .cursor_pointer()
            .text_sm()
    )
    .child(
        v_flex()
            .gap_2()
            .child(
                div()
                    .child("This is a hover card")
                    .font_semibold()
                    .text_sm()
            )
            .child(
                div()
                    .child("You can display rich content when hovering over a trigger element.")
                    .text_color(cx.theme().muted_foreground)
                    .text_sm()
            )
    )
HoverCard {
    title: "Ada Lovelace";
    description: "Mathematician and the first computer programmer.";
    Button {
        text: "@ada";
        variant: ButtonVariant.link;
    }
}
// The whole component: a trigger, a title and a description. The card opens
// once the pointer has rested on the trigger for the delay.
let hover = HoverCard::new(trigger("@ada"))
    .title("Ada Lovelace")
    .description("Mathematician and the first computer programmer.");

#User Profile Preview

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// A card can open over any hoverable host, and its content is the caller's
// title and description, so the same component stands in for a profile and
// for a channel.
let profiles = row![
    HoverCard::new(trigger("Open profile"))
        .title("grace@example.com")
        .description("Maintains the Slint port. Reviewed 42 changes this month."),
    HoverCard::new(trigger("Join channel"))
        .title("#design")
        .description("12 members. Last message 4 minutes ago."),
]
.spacing(theme::SPACING_LG)
.align_y(Alignment::Center);

A common use case is showing user profiles when hovering over a username, similar to GitHub or Twitter:

use gpui_kit::{px, relative, Styled as _};
use gpui_kit::component::{
    avatar::Avatar,
    hover_card::HoverCard,
    h_flex,
    v_flex,
};

h_flex()
    .child("Hover over ")
    .text_sm()
    .child(
        HoverCard::new("user-profile")
            .trigger(
                div()
                    .child("@huacnlee")
                    .cursor_pointer()
                    .text_color(cx.theme().link)
            )
            .child(
                h_flex()
                    .w(px(320.))
                    .gap_4()
                    .items_start()
                    .child(
                        Avatar::new()
                            .src("https://avatars.githubusercontent.com/u/5518?s=64")
                    )
                    .child(
                        v_flex()
                            .gap_1()
                            .line_height(relative(1.))
                            .child(div().child("Jason Lee").font_semibold())
                            .child(
                                div()
                                    .child("@huacnlee")
                                    .text_color(cx.theme().muted_foreground)
                                    .text_sm()
                            )
                            .child("The author of uni-kit.")
                    )
            )
    )
    .child(" to see their profile")
HorizontalLayout {
    spacing: 16px;
    alignment: start;
    HoverCard {
        title: "grace@example.com";
        description: "Maintains the Slint port. Reviewed 42 changes this month.";
        Button {
            text: "Open profile";
            variant: ButtonVariant.link;
        }
    }
    HoverCard {
        title: "#design";
        description: "12 members. Last message 4 minutes ago.";
        Button {
            text: "Join channel";
            variant: ButtonVariant.link;
        }
    }
}

#Custom Timing

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// `delay` is the only timing control Slint has: zero opens as soon as the
// pointer arrives, and a longer delay lets a passing pointer go by.
let timing = column![
    HoverCard::new(trigger("No delay"))
        .title("Immediate")
        .description("Opens as soon as the pointer rests.")
        .delay(Duration::ZERO),
    HoverCard::new(trigger("600 ms"))
        .title("Deliberate")
        .description("Waits before opening, so a passing pointer does not flash it.")
        .delay(Duration::from_millis(600)),
]
.spacing(theme::SPACING_LG)
.align_x(Alignment::Start);

Adjust the opening and closing delays to suit your needs:

use std::time::Duration;
use gpui_kit::Styled as _;
use gpui_kit::component::{
    button::{Button, ButtonVariants as _},
    h_flex,
};

h_flex()
    .gap_4()
    .child(
        HoverCard::new("fast-open")
            .open_delay(Duration::from_millis(200))
            .close_delay(Duration::from_millis(100))
            .trigger(Button::new("fast").label("Fast Open (200ms)").outline())
            .child(div().child("This hover card opens after 200ms").text_sm())
    )
    .child(
        HoverCard::new("slow-open")
            .open_delay(Duration::from_secs(1))
            .close_delay(Duration::from_secs_f32(0.5))
            .trigger(Button::new("slow").label("Slow Open (1000ms)").outline())
            .child(div().child("This hover card opens after 1000ms").text_sm())
    )
HoverCard {
    title: "Immediate";
    description: "Opens as soon as the pointer rests.";
    delay: 0ms;
    Button {
        text: "No delay";
        variant: ButtonVariant.link;
    }
}
HoverCard {
    title: "Deliberate";
    description: "Waits before opening, so a passing pointer does not flash it.";
    delay: 600ms;
    Button {
        text: "600 ms";
        variant: ButtonVariant.link;
    }
}

#Positioning

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The four sides, top, right, left and bottom, in a centred cluster with
// room on every side. Each card prefers the side it is given and only flips
// to the opposite one when the frame has no room for it, so the cluster
// leaves enough space around each trigger for its card to open where it says.
let positioning = container(
    column![
        row![
            cell(
                HoverCard::new(trigger("Top"))
                    .placement(Side::Top)
                    .title("Above")
                    .description("Prefers the side above.")
            ),
            cell(
                HoverCard::new(trigger("Right"))
                    .placement(Side::Right)
                    .title("Right")
                    .description("Prefers the side to the right.")
            ),
        ]
        .spacing(theme::SPACING_XXL),
        row![
            cell(
                HoverCard::new(trigger("Left"))
                    .placement(Side::Left)
                    .title("Left")
                    .description("Prefers the side to the left.")
            ),
            cell(
                HoverCard::new(trigger("Bottom"))
                    .placement(Side::Bottom)
                    .title("Below")
                    .description("Prefers the side below.")
            ),
        ]
        .spacing(theme::SPACING_XXL),
    ]
    .spacing(theme::SPACING_LG),
)
.width(Length::Fill)
.align_x(Alignment::Center)
.padding(Padding {
    top: 48.0,
    bottom: 48.0,
    left: 0.0,
    right: 0.0,
});

HoverCard supports 6 positioning options using the Anchor type:

  • TopLeft
  • TopCenter
  • TopRight
  • BottomLeft
  • BottomCenter
  • BottomRight

Imagine the card has a pointer tip (like a speech bubble’s tail). The anchor is where that tip sits relative to the trigger — TopCenter places it at the trigger’s top center, BottomRight at the bottom-right, and so on. The card then hangs off that point.

For example, Anchor::TopLeft places the card just below the trigger, left-aligned to it:

[ Trigger ]
┌──────────────┐
│  Hover Card  │
└──────────────┘
HorizontalLayout {
    horizontal-stretch: 1;
    spacing: 16px;
    HoverCard {
        title: "Start";
        description: "Anchored to this label.";
        Button {
            text: "Left";
            variant: ButtonVariant.link;
        }
    }
    HoverCard {
        title: "End";
        description: "Anchored to the trailing control.";
        Button {
            text: "Right";
            variant: ButtonVariant.link;
        }
    }
}

#Custom Content Builder

For performance optimization, you can provide a content builder function for more complex case, which only calls when the HoverCard is opened:

HoverCard::new("complex")
    .trigger(Button::new("btn").label("Hover me"))
    .content(|state, window, cx| {
        v_flex()
            .child("Dynamic content")
            .child(format!("Open: {}", state.is_open()))
    })
SlintNo Slint version of this example yet.

#Styling

HoverCard inherits all Styled trait methods:

HoverCard::new("styled")
    .trigger(Button::new("btn").label("Styled"))
    .w(px(400.))
    .max_h(px(500.))
    .text_sm()
    .gap_2()
    .child("Styled content")
SlintNo Slint version of this example yet.

Disable default appearance and apply custom styles:

HoverCard::new("custom-styled")
    .appearance(false)  // Disable default popover styling
    .trigger(Button::new("btn").label("Custom"))
    .bg(cx.theme().background)
    .border_2()
    .border_color(cx.theme().primary)
    .rounded(px(12.))
    .p_4()
    .child("Custom styled content")
SlintNo Slint version of this example yet.

#Behavior Details

#Hover Timing

The HoverCard uses a sophisticated timing system to provide a smooth user experience:

  1. Open Delay (600ms default): Prevents the card from flickering when the mouse quickly passes over the trigger
  2. Close Delay (300ms default): Gives users time to move their mouse from the trigger to the content area without the card closing
  3. Interactive Content: Users can move their mouse into the content area, and the card will remain open as long as the mouse is either on the trigger or in the content

#Edge Cases Handled

  • Quick Mouse Sweep: If the mouse quickly moves across the trigger, the card won’t open (cancelled by the open delay)
  • Trigger to Content Movement: The card stays open when moving the mouse from the trigger to the content area
  • Rapid Hovering: Multiple rapid hover events are debounced using an epoch-based timer system
  • Multiple HoverCards: Each HoverCard has independent state, so multiple cards can coexist without interfering

#Best Practices

  1. Use appropriate delays:

    • Standard content: 600ms open, 300ms close
    • Quick previews: 500ms open, 200ms close
    • Tooltips: 300ms open, 100ms close
  2. Keep content concise: HoverCards should provide preview information, not full content

  3. Make triggers visually distinct: Use colors, underlines, or cursor changes to indicate hoverable elements

  4. Consider accessibility: HoverCards are visual-only and don’t support keyboard navigation. For keyboard-accessible content, consider using a Popover instead

  5. Avoid nested HoverCards: They can create confusing user experiences

#Differences from Popover

FeatureHoverCardPopover
TriggerMouse hoverClick/right-click
Keyboard navigationNoYes (with focus)
Dismiss on outside clickNoYes (configurable)
Timing delaysYes (open/close)No
Primary use casePreviewsActions/forms

#API Reference

#HoverCard Methods

  • new(id: impl Into<ElementId>) - Create a new HoverCard with a unique ID
  • trigger<T: IntoElement>(trigger: T) - Set the element that triggers the hover
  • content<F>(content: F) - Set a content builder function that receives (&mut HoverCardState, &mut Window, &mut Context<HoverCardState>)
  • open_delay(duration: Duration) - Set delay before showing (default: 600ms)
  • close_delay(duration: Duration) - Set delay before hiding (default: 300ms)
  • anchor(anchor: impl Into<Anchor>) - Set positioning (default: TopCenter)
  • on_open_change<F>(callback: F) - Callback when open state changes, receives (&bool, &mut Window, &mut App)
  • appearance(appearance: bool) - Enable/disable default styling (default: true)

#HoverCardState Methods

  • is_open() -> bool - Check if the hover card is currently open

#HoverCard

Richer hover content. Mirrors GPUI Component's `HoverCard`.

NameKindTypeDefault
titleinstring
descriptioninstring
delayinduration280ms