diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs
index ffe6fea7ea447..b00cefdddb524 100644
--- a/src/librustdoc/clean/types.rs
+++ b/src/librustdoc/clean/types.rs
@@ -482,16 +482,16 @@ impl Item {
     }
 
     pub(crate) fn links(&self, cx: &Context<'_>) -> Vec<RenderedLink> {
-        use crate::html::format::href;
+        use crate::html::format::{href, link_tooltip};
 
         cx.cache()
             .intra_doc_links
             .get(&self.item_id)
             .map_or(&[][..], |v| v.as_slice())
             .iter()
-            .filter_map(|ItemLink { link: s, link_text, page_id: did, ref fragment }| {
-                debug!(?did);
-                if let Ok((mut href, ..)) = href(*did, cx) {
+            .filter_map(|ItemLink { link: s, link_text, page_id: id, ref fragment }| {
+                debug!(?id);
+                if let Ok((mut href, ..)) = href(*id, cx) {
                     debug!(?href);
                     if let Some(ref fragment) = *fragment {
                         fragment.render(&mut href, cx.tcx())
@@ -499,6 +499,7 @@ impl Item {
                     Some(RenderedLink {
                         original_text: s.clone(),
                         new_text: link_text.clone(),
+                        tooltip: link_tooltip(*id, fragment, cx),
                         href,
                     })
                 } else {
@@ -523,6 +524,7 @@ impl Item {
                 original_text: s.clone(),
                 new_text: link_text.clone(),
                 href: String::new(),
+                tooltip: String::new(),
             })
             .collect()
     }
@@ -1040,6 +1042,8 @@ pub struct RenderedLink {
     pub(crate) new_text: String,
     /// The URL to put in the `href`
     pub(crate) href: String,
+    /// The tooltip.
+    pub(crate) tooltip: String,
 }
 
 /// The attributes on an [`Item`], including attributes like `#[derive(...)]` and `#[inline]`,
diff --git a/src/librustdoc/html/format.rs b/src/librustdoc/html/format.rs
index 8a7a8ea5fd1f2..314f061224940 100644
--- a/src/librustdoc/html/format.rs
+++ b/src/librustdoc/html/format.rs
@@ -34,6 +34,7 @@ use crate::clean::{
 use crate::formats::item_type::ItemType;
 use crate::html::escape::Escape;
 use crate::html::render::Context;
+use crate::passes::collect_intra_doc_links::UrlFragment;
 
 use super::url_parts_builder::estimate_item_path_byte_length;
 use super::url_parts_builder::UrlPartsBuilder;
@@ -768,6 +769,21 @@ pub(crate) fn href_relative_parts<'fqp>(
     }
 }
 
+pub(crate) fn link_tooltip(did: DefId, fragment: &Option<UrlFragment>, cx: &Context<'_>) -> String {
+    let cache = cx.cache();
+    let Some((fqp, shortty)) = cache.paths.get(&did)
+        .or_else(|| cache.external_paths.get(&did))
+        else { return String::new() };
+    let fqp = fqp.iter().map(|sym| sym.as_str()).join("::");
+    if let &Some(UrlFragment::Item(id)) = fragment {
+        let name = cx.tcx().item_name(id);
+        let descr = cx.tcx().def_kind(id).descr(id);
+        format!("{descr} {fqp}::{name}")
+    } else {
+        format!("{shortty} {fqp}")
+    }
+}
+
 /// Used to render a [`clean::Path`].
 fn resolved_path<'cx>(
     w: &mut fmt::Formatter<'_>,
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index 331804393938f..e4adee6ae4dfb 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -360,6 +360,9 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
                     trace!("it matched");
                     assert!(self.shortcut_link.is_none(), "shortcut links cannot be nested");
                     self.shortcut_link = Some(link);
+                    if title.is_empty() && !link.tooltip.is_empty() {
+                        *title = CowStr::Borrowed(link.tooltip.as_ref());
+                    }
                 }
             }
             // Now that we're done with the shortcut link, don't replace any more text.
@@ -410,9 +413,12 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
             }
             // If this is a link, but not a shortcut link,
             // replace the URL, since the broken_link_callback was not called.
-            Some(Event::Start(Tag::Link(_, dest, _))) => {
+            Some(Event::Start(Tag::Link(_, dest, title))) => {
                 if let Some(link) = self.links.iter().find(|&link| *link.original_text == **dest) {
                     *dest = CowStr::Borrowed(link.href.as_ref());
+                    if title.is_empty() && !link.tooltip.is_empty() {
+                        *title = CowStr::Borrowed(link.tooltip.as_ref());
+                    }
                 }
             }
             // Anything else couldn't have been a valid Rust path, so no need to replace the text.
@@ -976,7 +982,7 @@ impl Markdown<'_> {
             links
                 .iter()
                 .find(|link| link.original_text.as_str() == &*broken_link.reference)
-                .map(|link| (link.href.as_str().into(), link.new_text.as_str().into()))
+                .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
         };
 
         let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(&mut replacer));
@@ -1059,7 +1065,7 @@ impl MarkdownSummaryLine<'_> {
             links
                 .iter()
                 .find(|link| link.original_text.as_str() == &*broken_link.reference)
-                .map(|link| (link.href.as_str().into(), link.new_text.as_str().into()))
+                .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
         };
 
         let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer))
@@ -1106,7 +1112,7 @@ fn markdown_summary_with_limit(
         link_names
             .iter()
             .find(|link| link.original_text.as_str() == &*broken_link.reference)
-            .map(|link| (link.href.as_str().into(), link.new_text.as_str().into()))
+            .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
     };
 
     let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
@@ -1187,7 +1193,7 @@ pub(crate) fn plain_text_summary(md: &str, link_names: &[RenderedLink]) -> Strin
         link_names
             .iter()
             .find(|link| link.original_text.as_str() == &*broken_link.reference)
-            .map(|link| (link.href.as_str().into(), link.new_text.as_str().into()))
+            .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
     };
 
     let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
diff --git a/tests/rustdoc/intra-doc/basic.rs b/tests/rustdoc/intra-doc/basic.rs
index 39f5c298bc4a1..e2d3ef425cb45 100644
--- a/tests/rustdoc/intra-doc/basic.rs
+++ b/tests/rustdoc/intra-doc/basic.rs
@@ -1,21 +1,38 @@
 // @has basic/index.html
 // @has - '//a/@href' 'struct.ThisType.html'
+// @has - '//a/@title' 'struct basic::ThisType'
 // @has - '//a/@href' 'struct.ThisType.html#method.this_method'
+// @has - '//a/@title' 'associated function basic::ThisType::this_method'
 // @has - '//a/@href' 'enum.ThisEnum.html'
+// @has - '//a/@title' 'enum basic::ThisEnum'
 // @has - '//a/@href' 'enum.ThisEnum.html#variant.ThisVariant'
+// @has - '//a/@title' 'variant basic::ThisEnum::ThisVariant'
 // @has - '//a/@href' 'trait.ThisTrait.html'
+// @has - '//a/@title' 'trait basic::ThisTrait'
 // @has - '//a/@href' 'trait.ThisTrait.html#tymethod.this_associated_method'
+// @has - '//a/@title' 'associated function basic::ThisTrait::this_associated_method'
 // @has - '//a/@href' 'trait.ThisTrait.html#associatedtype.ThisAssociatedType'
+// @has - '//a/@title' 'associated type basic::ThisTrait::ThisAssociatedType'
 // @has - '//a/@href' 'trait.ThisTrait.html#associatedconstant.THIS_ASSOCIATED_CONST'
+// @has - '//a/@title' 'associated constant basic::ThisTrait::THIS_ASSOCIATED_CONST'
 // @has - '//a/@href' 'trait.ThisTrait.html'
+// @has - '//a/@title' 'trait basic::ThisTrait'
 // @has - '//a/@href' 'type.ThisAlias.html'
+// @has - '//a/@title' 'type basic::ThisAlias'
 // @has - '//a/@href' 'union.ThisUnion.html'
+// @has - '//a/@title' 'union basic::ThisUnion'
 // @has - '//a/@href' 'fn.this_function.html'
+// @has - '//a/@title' 'fn basic::this_function'
 // @has - '//a/@href' 'constant.THIS_CONST.html'
+// @has - '//a/@title' 'constant basic::THIS_CONST'
 // @has - '//a/@href' 'static.THIS_STATIC.html'
+// @has - '//a/@title' 'static basic::THIS_STATIC'
 // @has - '//a/@href' 'macro.this_macro.html'
+// @has - '//a/@title' 'macro basic::this_macro'
 // @has - '//a/@href' 'trait.SoAmbiguous.html'
+// @has - '//a/@title' 'trait basic::SoAmbiguous'
 // @has - '//a/@href' 'fn.SoAmbiguous.html'
+// @has - '//a/@title' 'fn basic::SoAmbiguous'
 //! In this crate we would like to link to:
 //!
 //! * [`ThisType`](ThisType)