diff --git a/web_src/js/markup/codecopy.js b/web_src/js/markup/codecopy.js
index 2aa7070c72..a12802ef73 100644
--- a/web_src/js/markup/codecopy.js
+++ b/web_src/js/markup/codecopy.js
@@ -1,15 +1,18 @@
 import {svg} from '../svg.js';
 
+export function makeCodeCopyButton() {
+  const button = document.createElement('button');
+  button.classList.add('code-copy', 'ui', 'button');
+  button.innerHTML = svg('octicon-copy');
+  return button;
+}
+
 export function renderCodeCopy() {
   const els = document.querySelectorAll('.markup .code-block code');
   if (!els.length) return;
 
-  const button = document.createElement('button');
-  button.classList.add('code-copy', 'ui', 'button');
-  button.innerHTML = svg('octicon-copy');
-
   for (const el of els) {
-    const btn = button.cloneNode(true);
+    const btn = makeCodeCopyButton();
     btn.setAttribute('data-clipboard-text', el.textContent);
     el.after(btn);
   }
diff --git a/web_src/js/markup/mermaid.js b/web_src/js/markup/mermaid.js
index efd9abbb56..29fa92552d 100644
--- a/web_src/js/markup/mermaid.js
+++ b/web_src/js/markup/mermaid.js
@@ -1,4 +1,6 @@
 import {isDarkTheme} from '../utils.js';
+import {makeCodeCopyButton} from './codecopy.js';
+
 const {mermaidMaxSourceCharacters} = window.config;
 
 const iframeCss = `
@@ -58,7 +60,13 @@ export async function renderMermaid() {
         iframe.sandbox = 'allow-scripts';
         iframe.style.height = `${Math.ceil(parseFloat(heightStr))}px`;
         iframe.srcdoc = `<html><head><style>${iframeCss}</style></head><body>${svgStr}</body></html>`;
-        el.closest('pre').replaceWith(iframe);
+        const mermaidBlock = document.createElement('div');
+        mermaidBlock.classList.add('mermaid-block');
+        mermaidBlock.append(iframe);
+        const btn = makeCodeCopyButton();
+        btn.setAttribute('data-clipboard-text', source);
+        mermaidBlock.append(btn);
+        el.closest('pre').replaceWith(mermaidBlock);
       });
     } catch (err) {
       displayError(el, err);
diff --git a/web_src/js/modules/tippy.js b/web_src/js/modules/tippy.js
index 6a89151691..ce8f0369f1 100644
--- a/web_src/js/modules/tippy.js
+++ b/web_src/js/modules/tippy.js
@@ -6,6 +6,7 @@ export function createTippy(target, opts = {}) {
     placement: target.getAttribute('data-placement') || 'top-start',
     animation: false,
     allowHTML: false,
+    hideOnClick: false,
     interactiveBorder: 30,
     ignoreAttributes: true,
     maxWidth: 500, // increase over default 350px
@@ -46,7 +47,7 @@ export function showTemporaryTooltip(target, content) {
   }
 
   tippy.setContent(content);
-  tippy.show();
+  if (!tippy.state.isShown) tippy.show();
   tippy.setProps({
     onHidden: (tippy) => {
       if (oldContent) {
diff --git a/web_src/less/markup/codecopy.less b/web_src/less/markup/codecopy.less
index b2ce77aaa1..f6f9894fc1 100644
--- a/web_src/less/markup/codecopy.less
+++ b/web_src/less/markup/codecopy.less
@@ -1,4 +1,5 @@
-.markup .code-block {
+.markup .code-block,
+.markup .mermaid-block {
   position: relative;
 }
 
@@ -26,7 +27,8 @@
   background: var(--color-secondary-dark-1) !important;
 }
 
-.markup .code-block:hover .code-copy {
+.markup .code-block:hover .code-copy,
+.markup .mermaid-block:hover .code-copy {
   visibility: visible;
   animation: fadein .2s both;
 }