diff --git a/news/changelog-1.8.md b/news/changelog-1.8.md
index ec10b18d463..861ae795f41 100644
--- a/news/changelog-1.8.md
+++ b/news/changelog-1.8.md
@@ -20,6 +20,7 @@ All changes included in 1.8:
 - ([#12747](https://github.com/quarto-dev/quarto-cli/issues/12747)): Ensure `th` elements are properly restored when Quarto's HTML table processing is happening.
 - ([#12766](https://github.com/quarto-dev/quarto-cli/issues/12766)): Use consistent equation numbering display for `html-math-method` and `html-math-method.method` for MathJax and KaTeX (author: @mcanouil)
 - ([#12797](https://github.com/quarto-dev/quarto-cli/issues/12797)): Allow light and dark brands to be specified in one file, by specializing colors with `light:` and `dark:`.
+- ([#12919](https://github.com/quarto-dev/quarto-cli/issues/12919)): Ensure `kbd` shortcode output has hover tooltip.
 
 ### `revealjs`
 
diff --git a/src/resources/extensions/quarto/kbd/kbd.lua b/src/resources/extensions/quarto/kbd/kbd.lua
index f65e5a38b5b..ae0a8658177 100644
--- a/src/resources/extensions/quarto/kbd/kbd.lua
+++ b/src/resources/extensions/quarto/kbd/kbd.lua
@@ -13,8 +13,10 @@ return {
         stylesheets = { 'resources/kbd.css' }
       })
       local kwargs_strs = {}
+      local title_strs = {}
       for k, v in pairs(kwargs) do
         table.insert(kwargs_strs, string.format('data-%s="%s"', osname(k), pandoc.utils.stringify(v)))
+        table.insert(title_strs, osname(k) .. ': ' .. pandoc.utils.stringify(v))
       end
       table.sort(kwargs_strs) -- sort so that the output is deterministic
       local kwargs_str = table.concat(kwargs_strs)
@@ -29,9 +31,14 @@ return {
         default_arg_str = ""
       else
         default_arg_str = pandoc.utils.stringify(args[1])
+        table.insert(title_strs, default_arg_str)
       end
-
-      return pandoc.RawInline('html', '' .. default_arg_str .. '' .. default_arg_str .. '')
+      table.sort(title_strs) -- sort so that the output is deterministic
+      local title_str = table.concat(title_strs, ', ')
+      if title_str == "" then
+        title_str = default_arg_str
+      end
+      return pandoc.RawInline('html', '' .. default_arg_str .. '' .. default_arg_str .. '')
     elseif quarto.doc.isFormat("asciidoc") then
       if args and #args == 1 then
         -- https://docs.asciidoctor.org/asciidoc/latest/macros/keyboard-macro/
diff --git a/tests/docs/smoke-all/2025/06/12/issue-12919.qmd b/tests/docs/smoke-all/2025/06/12/issue-12919.qmd
new file mode 100644
index 00000000000..67bfdcefac7
--- /dev/null
+++ b/tests/docs/smoke-all/2025/06/12/issue-12919.qmd
@@ -0,0 +1,12 @@
+---
+format: html
+_quarto:
+  tests:
+    html:
+      ensureHtmlElements:
+        - ['kbd[title="Shift-K"]','kbd[title="linux: Shift-Ctrl-L, mac: Shift-Command-O, windows: Shift-Control-O"]']
+---
+
+{{< kbd mac=Shift-Command-O win=Shift-Control-O linux=Shift-Ctrl-L >}}
+
+{{< kbd Shift-K >}}
\ No newline at end of file