diff --git a/assets/js/src/tooltip.js b/assets/js/src/tooltip.js
new file mode 100644
index 0000000000..79db56cf43
--- /dev/null
+++ b/assets/js/src/tooltip.js
@@ -0,0 +1,66 @@
+import { computePosition, flip, shift, offset, arrow } from "@floating-ui/dom";
+
+/* Regular tooltips (partial) */
+
+const tooltipWrappers = Array.from(
+ document.querySelectorAll("[data-tooltip-wrapper]"),
+);
+
+for (const tooltipWrapper of tooltipWrappers) {
+ const button = tooltipWrapper.firstElementChild;
+ const tooltip = button.nextElementSibling;
+ const arrowElement = tooltip.firstElementChild;
+
+ function update() {
+ computePosition(button, tooltip, {
+ placement: "top",
+ middleware: [
+ offset(6),
+ flip(),
+ shift({ padding: 5 }),
+ arrow({ element: arrowElement }),
+ ],
+ }).then(({ x, y, placement, middlewareData }) => {
+ Object.assign(tooltip.style, {
+ left: `${x}px`,
+ top: `${y}px`,
+ });
+
+ // Accessing the data
+ const { x: arrowX, y: arrowY } = middlewareData.arrow;
+
+ const staticSide = {
+ top: "bottom",
+ right: "left",
+ bottom: "top",
+ left: "right",
+ }[placement.split("-")[0]];
+
+ Object.assign(arrowElement.style, {
+ left: arrowX != null ? `${arrowX}px` : "",
+ top: arrowY != null ? `${arrowY}px` : "",
+ right: "",
+ bottom: "",
+ [staticSide]: "-4px",
+ });
+ });
+ }
+
+ function showTooltip() {
+ tooltip.classList.toggle("hidden");
+ update();
+ }
+
+ function hideTooltip() {
+ tooltip.classList.toggle("hidden");
+ }
+
+ [
+ ["mouseenter", showTooltip],
+ ["mouseleave", hideTooltip],
+ ["focus", showTooltip],
+ ["blur", hideTooltip],
+ ].forEach(([event, listener]) => {
+ button.addEventListener(event, listener);
+ });
+}
diff --git a/layouts/partials/tooltip.html b/layouts/partials/tooltip.html
new file mode 100644
index 0000000000..06fa905ffb
--- /dev/null
+++ b/layouts/partials/tooltip.html
@@ -0,0 +1,11 @@
+
+
+ {{ partial "icon" "help" }}
+
+
+
diff --git a/package-lock.json b/package-lock.json
index 3ab08522d2..9e33f2957f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"dependencies": {
"@alpinejs/collapse": "^3.13.5",
"@docsearch/js": "^3.5.2",
+ "@floating-ui/dom": "^1.6.3",
"@material-symbols/svg-400": "^0.14.6",
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
"@tailwindcss/typography": "^0.5.10",
@@ -244,6 +245,28 @@
}
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz",
+ "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.1"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz",
+ "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==",
+ "dependencies": {
+ "@floating-ui/core": "^1.0.0",
+ "@floating-ui/utils": "^0.2.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
+ "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
+ },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
diff --git a/package.json b/package.json
index 58a67a0476..51341f773e 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"dependencies": {
"@alpinejs/collapse": "^3.13.5",
"@docsearch/js": "^3.5.2",
+ "@floating-ui/dom": "^1.6.3",
"@material-symbols/svg-400": "^0.14.6",
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
"@tailwindcss/typography": "^0.5.10",